canvaslms-cli 1.3.3 → 1.4.0

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.
@@ -1,80 +1,102 @@
1
- /**
2
- * Announcements command
3
- */
4
-
5
- const { makeCanvasRequest } = require('../lib/api-client');
6
-
7
- async function showAnnouncements(courseId, options) {
8
- try {
9
- if (courseId) {
10
- // Get course information first
11
- const course = await makeCanvasRequest('get', `courses/${courseId}`);
12
-
13
- // Get announcements for specific course
14
- const announcements = await makeCanvasRequest('get', `courses/${courseId}/discussion_topics`, [
15
- 'only_announcements=true',
16
- `per_page=${options.limit}`
17
- ]);
18
-
19
- if (!announcements || announcements.length === 0) {
20
- console.log(`No announcements found for course: ${course.name}`);
21
- return;
22
- }
23
-
24
- console.log(`📢 Recent ${announcements.length} announcement(s) for: ${course.name}\n`);
25
-
26
- announcements.forEach((announcement, index) => {
27
- console.log(`${index + 1}. ${announcement.title}`);
28
- console.log(` Posted: ${announcement.posted_at ? new Date(announcement.posted_at).toLocaleString() : 'N/A'}`);
29
- console.log(` Author: ${announcement.author?.display_name || 'Unknown'}`);
30
-
31
- if (announcement.message) {
32
- // Remove HTML tags and truncate
33
- const message = announcement.message.replace(/<[^>]*>/g, '').substring(0, 200);
34
- console.log(` Message: ${message}${announcement.message.length > 200 ? '...' : ''}`);
35
- }
36
-
37
- console.log('');
38
- });
39
-
40
- } else {
41
- // Get announcements for all enrolled courses
42
- const courses = await makeCanvasRequest('get', 'courses', ['enrollment_state=active']);
43
-
44
- if (!courses || courses.length === 0) {
45
- console.log('No courses found.');
46
- return;
47
- }
48
-
49
- console.log('Getting announcements from all enrolled courses...\n');
50
-
51
- for (const course of courses.slice(0, 3)) { // Limit to first 3 courses to avoid too many requests
52
- try {
53
- const announcements = await makeCanvasRequest('get', `courses/${course.id}/discussion_topics`, [
54
- 'only_announcements=true',
55
- 'per_page=2'
56
- ]);
57
-
58
- if (announcements && announcements.length > 0) {
59
- console.log(`📢 ${course.name}:`);
60
- announcements.forEach((announcement) => {
61
- console.log(` • ${announcement.title} (${announcement.posted_at ? new Date(announcement.posted_at).toLocaleDateString() : 'No date'})`);
62
- });
63
- console.log('');
64
- }
65
- } catch (error) {
66
- // Skip courses that don't allow access to announcements
67
- continue;
68
- }
69
- }
70
- }
71
-
72
- } catch (error) {
73
- console.error('Error fetching announcements:', error.message);
74
- process.exit(1);
75
- }
76
- }
77
-
78
- module.exports = {
79
- showAnnouncements
80
- };
1
+ import { makeCanvasRequest } from '../lib/api-client.js';
2
+ import { createReadlineInterface, askQuestion } from '../lib/interactive.js';
3
+ import chalk from 'chalk';
4
+
5
+ export async function showAnnouncements(courseId, options) {
6
+ const rl = createReadlineInterface();
7
+ try {
8
+ if (!courseId) {
9
+ console.log(chalk.cyan.bold('Loading your courses, please wait...\n'));
10
+ const courses = await makeCanvasRequest('get', 'courses', [
11
+ 'enrollment_state=active',
12
+ 'include[]=favorites'
13
+ ]);
14
+
15
+ if (!courses || courses.length === 0) {
16
+ console.log(chalk.red('Error: No courses found.'));
17
+ rl.close();
18
+ return;
19
+ }
20
+
21
+ console.log(chalk.cyan.bold('\n' + '-'.repeat(60)));
22
+ console.log(chalk.cyan.bold('Select a course:'));
23
+ courses.forEach((course, index) => {
24
+ console.log(chalk.white(`${index + 1}.`) + ' ' + course.name);
25
+ });
26
+
27
+ const courseChoice = await askQuestion(rl, chalk.white('\nEnter course number: '));
28
+ if (!courseChoice.trim()) {
29
+ console.log(chalk.red('No course selected. Exiting...'));
30
+ rl.close();
31
+ return;
32
+ }
33
+
34
+ const courseIndex = parseInt(courseChoice) - 1;
35
+ if (courseIndex < 0 || courseIndex >= courses.length) {
36
+ console.log(chalk.red('Invalid course selection.'));
37
+ rl.close();
38
+ return;
39
+ }
40
+
41
+ courseId = courses[courseIndex].id;
42
+ console.log(chalk.green(`Success: Selected ${courses[courseIndex].name}\n`));
43
+ }
44
+
45
+ // FIXED: Use discussion_topics endpoint with only_announcements=true
46
+ const limit = parseInt(options.limit) || 5;
47
+ const announcements = await makeCanvasRequest(
48
+ 'get',
49
+ `courses/${courseId}/discussion_topics`,
50
+ [`only_announcements=true`, `per_page=${limit}`]
51
+ );
52
+
53
+ if (!announcements || announcements.length === 0) {
54
+ console.log(chalk.yellow('No announcements found for this course.'));
55
+ rl.close();
56
+ return;
57
+ }
58
+
59
+ console.log(chalk.green('Success: Announcement loaded.'));
60
+ console.log(chalk.cyan.bold('\n' + '-'.repeat(60)));
61
+ console.log(chalk.cyan.bold('Announcements:'));
62
+ announcements.forEach((a, i) => {
63
+ const date = a.posted_at ? new Date(a.posted_at).toLocaleDateString() : '';
64
+ console.log(chalk.white(`${i + 1}.`) + ' ' + a.title + chalk.gray(date ? ` (${date})` : ''));
65
+ });
66
+
67
+ const annChoice = await askQuestion(rl, chalk.white('\nEnter announcement number to view details: '));
68
+ if (!annChoice.trim()) {
69
+ console.log(chalk.red('No announcement selected. Exiting...'));
70
+ rl.close();
71
+ return;
72
+ }
73
+
74
+ const annIndex = parseInt(annChoice) - 1;
75
+ if (annIndex < 0 || annIndex >= announcements.length) {
76
+ console.log(chalk.red('Invalid announcement selection.'));
77
+ rl.close();
78
+ return;
79
+ }
80
+
81
+ const ann = announcements[annIndex];
82
+ const title = ann.title || 'Untitled';
83
+ const date = ann.posted_at ? new Date(ann.posted_at).toLocaleString() : 'N/A';
84
+ const author = ann.author?.display_name || 'Unknown';
85
+ const message = ann.message?.replace(/<[^>]+>/g, '').trim() || 'No content';
86
+
87
+ console.log('\n' + chalk.cyan('='.repeat(60)));
88
+ console.log(chalk.bold(' ' + title));
89
+ console.log(chalk.cyan('-'.repeat(60)));
90
+ console.log(chalk.gray(' Posted: ') + date);
91
+ console.log(chalk.gray(' Author: ') + author);
92
+ console.log('\n' + chalk.bold(' Message:') + '\n');
93
+ console.log(message);
94
+ console.log(chalk.cyan('='.repeat(60)) + '\n');
95
+
96
+ } catch (error) {
97
+ console.error(chalk.red('Error: Failed to fetch announcements: ') + error.message);
98
+ } finally {
99
+ rl.close();
100
+ }
101
+ }
102
+
@@ -1,130 +1,100 @@
1
- /**
2
- * Assignments command
3
- */
4
-
5
- const { makeCanvasRequest } = require('../lib/api-client');
6
-
7
- async function listAssignments(courseId, options) {
8
- try {
9
- // First get course information to display course name
10
- const course = await makeCanvasRequest('get', `courses/${courseId}`);
11
-
12
- const queryParams = ['include[]=submission', 'include[]=score_statistics', 'per_page=100'];
13
-
14
- const assignments = await makeCanvasRequest('get', `courses/${courseId}/assignments`, queryParams);
15
-
16
- if (!assignments || assignments.length === 0) {
17
- console.log(`No assignments found for course: ${course.name}`);
18
- return;
19
- }
20
-
21
- // Filter assignments based on options
22
- let filteredAssignments = assignments;
23
- if (options.submitted) {
24
- filteredAssignments = assignments.filter(a => a.submission && a.submission.submitted_at);
25
- } else if (options.pending) {
26
- filteredAssignments = assignments.filter(a => !a.submission || !a.submission.submitted_at);
27
- }
28
-
29
- // Display course information prominently
30
- console.log(`📚 Course: ${course.name}`);
31
- console.log(`📝 Found ${filteredAssignments.length} assignment(s):\n`);
32
-
33
- filteredAssignments.forEach((assignment, index) => {
34
- const submission = assignment.submission;
35
- const isSubmitted = submission && submission.submitted_at;
36
- const submissionStatus = isSubmitted ? '' : '❌';
37
-
38
- // Format grade like Canvas web interface with enhanced formatting
39
- let gradeDisplay = '';
40
- let gradeColor = '';
41
-
42
- if (submission && submission.score !== null && submission.score !== undefined) {
43
- // Format score to remove unnecessary decimals (e.g., 3.0 becomes 3)
44
- const score = submission.score % 1 === 0 ? Math.round(submission.score) : submission.score;
45
- const total = assignment.points_possible || 0;
46
- gradeDisplay = `${score}/${total}`;
47
-
48
- // Add color coding based on score percentage
49
- if (total > 0) {
50
- const percentage = (submission.score / total) * 100;
51
- if (percentage >= 90) gradeColor = '\x1b[32m'; // Green for A
52
- else if (percentage >= 80) gradeColor = '\x1b[36m'; // Cyan for B
53
- else if (percentage >= 70) gradeColor = '\x1b[33m'; // Yellow for C
54
- else if (percentage >= 60) gradeColor = '\x1b[35m'; // Magenta for D
55
- else gradeColor = '\x1b[31m'; // Red for F
56
- }
57
- } else if (submission && submission.excused) {
58
- gradeDisplay = 'Excused';
59
- gradeColor = '\x1b[34m'; // Blue for excused
60
- } else if (submission && submission.missing) {
61
- gradeDisplay = 'Missing';
62
- gradeColor = '\x1b[31m'; // Red for missing
63
- } else if (assignment.points_possible) {
64
- gradeDisplay = `–/${assignment.points_possible}`;
65
- gradeColor = '\x1b[90m'; // Gray for not graded yet
66
- } else {
67
- gradeDisplay = 'N/A';
68
- gradeColor = '\x1b[90m'; // Gray for N/A
69
- }
70
-
71
- // Handle letter grades if present
72
- if (submission && submission.grade && isNaN(submission.grade)) {
73
- gradeDisplay = submission.grade + (assignment.points_possible ? ` (${gradeDisplay})` : '');
74
- }
75
- console.log(`${index + 1}. ${submissionStatus} ${assignment.name}`);
76
- console.log(` Assignment ID: ${assignment.id}`);
77
- console.log(` Grade: ${gradeColor}${gradeDisplay} pts\x1b[0m`);
78
- console.log(` Due: ${assignment.due_at ? new Date(assignment.due_at).toLocaleString() : 'No due date'}`);
79
-
80
- if (isSubmitted) {
81
- console.log(` Submitted: ${new Date(submission.submitted_at).toLocaleString()}`);
82
- if (submission.workflow_state) {
83
- // Color code submission status
84
- let statusColor = '';
85
- switch(submission.workflow_state) {
86
- case 'graded': statusColor = '\x1b[32m'; break; // Green
87
- case 'submitted': statusColor = '\x1b[33m'; break; // Yellow
88
- case 'pending_review': statusColor = '\x1b[36m'; break; // Cyan
89
- default: statusColor = '\x1b[37m'; // White
90
- }
91
- console.log(` Status: ${statusColor}${submission.workflow_state}\x1b[0m`);
92
- }
93
- } else {
94
- console.log(` Status: \x1b[31mNot submitted\x1b[0m`); // Red for not submitted
95
- }
96
-
97
- if (options.verbose) {
98
- if (assignment.description) {
99
- // Strip HTML tags and clean up description
100
- const cleanDescription = assignment.description
101
- .replace(/<[^>]*>/g, '') // Remove HTML tags
102
- .replace(/\s+/g, ' ') // Replace multiple whitespace with single space
103
- .trim() // Remove leading/trailing whitespace
104
- .substring(0, 150); // Limit to 150 characters
105
- console.log(` Description: ${cleanDescription}${cleanDescription.length === 150 ? '...' : ''}`);
106
- } else {
107
- console.log(` Description: N/A`);
108
- }
109
- console.log(` Submission Types: ${assignment.submission_types?.join(', ') || 'N/A'}`);
110
- console.log(` Published: ${assignment.published ? 'Yes' : 'No'}`);
111
- if (assignment.points_possible) {
112
- console.log(` Points Possible: ${assignment.points_possible}`);
113
- }
114
- if (submission && submission.attempt) {
115
- console.log(` Attempt: ${submission.attempt}`);
116
- }
117
- }
118
-
119
- console.log('');
120
- });
121
-
122
- } catch (error) {
123
- console.error('Error fetching assignments:', error.message);
124
- process.exit(1);
125
- }
126
- }
127
-
128
- module.exports = {
129
- listAssignments
130
- };
1
+ /**
2
+ * Assignments command
3
+ */
4
+
5
+ import { makeCanvasRequest } from '../lib/api-client.js';
6
+ import chalk from 'chalk';
7
+
8
+ function pad(str, len) {
9
+ return str + ' '.repeat(Math.max(0, len - str.length));
10
+ }
11
+
12
+ export async function listAssignments(courseId, options) {
13
+ try {
14
+ const course = await makeCanvasRequest('get', `courses/${courseId}`);
15
+ const queryParams = ['include[]=submission', 'include[]=score_statistics', 'per_page=100'];
16
+ console.log(chalk.cyan.bold('\n' + '-'.repeat(60)));
17
+ console.log(chalk.cyan.bold('Loading assignments, please wait...'));
18
+ const assignments = await makeCanvasRequest('get', `courses/${courseId}/assignments`, queryParams);
19
+ if (!assignments || assignments.length === 0) {
20
+ console.log(chalk.red(`Error: No assignments found for course: ${course.name}`));
21
+ return;
22
+ }
23
+ let filteredAssignments = assignments;
24
+ if (options.submitted) {
25
+ filteredAssignments = assignments.filter(a => a.submission && a.submission.submitted_at);
26
+ } else if (options.pending) {
27
+ filteredAssignments = assignments.filter(a => !a.submission || !a.submission.submitted_at);
28
+ }
29
+ console.log(chalk.cyan.bold('\n' + '-'.repeat(60)));
30
+ console.log(chalk.cyan.bold(`Assignments for: ${course.name}`));
31
+ console.log(chalk.cyan('-'.repeat(60)));
32
+ console.log(chalk.green(`Success: Found ${filteredAssignments.length} assignment(s).`));
33
+ console.log(chalk.cyan('-'.repeat(60)));
34
+ // Column headers
35
+ console.log(
36
+ pad(chalk.bold('No.'), 5) +
37
+ pad(chalk.bold('Assignment Name'), 35) +
38
+ pad(chalk.bold('ID'), 10) +
39
+ pad(chalk.bold('Grade'), 12) +
40
+ pad(chalk.bold('Due'), 22) +
41
+ (options.verbose ? pad(chalk.bold('Status'), 12) : '')
42
+ );
43
+ console.log(chalk.cyan('-'.repeat(60)));
44
+ filteredAssignments.forEach((assignment, index) => {
45
+ const submission = assignment.submission;
46
+ let gradeDisplay = '';
47
+ if (submission && submission.score !== null && submission.score !== undefined) {
48
+ const score = submission.score % 1 === 0 ? Math.round(submission.score) : submission.score;
49
+ const total = assignment.points_possible || 0;
50
+ gradeDisplay = `${score}/${total}`;
51
+ } else if (submission && submission.excused) {
52
+ gradeDisplay = 'Excused';
53
+ } else if (submission && submission.missing) {
54
+ gradeDisplay = 'Missing';
55
+ } else if (assignment.points_possible) {
56
+ gradeDisplay = `–/${assignment.points_possible}`;
57
+ } else {
58
+ gradeDisplay = 'N/A';
59
+ }
60
+ let line = pad(chalk.white((index + 1) + '.'), 5) +
61
+ pad(assignment.name, 35) +
62
+ pad(String(assignment.id), 10) +
63
+ pad(gradeDisplay, 12) +
64
+ pad(assignment.due_at ? new Date(assignment.due_at).toLocaleString() : 'No due date', 22);
65
+ if (options.verbose) {
66
+ let status = 'Not submitted';
67
+ if (submission && submission.submitted_at) {
68
+ status = 'Submitted';
69
+ }
70
+ line += pad(status, 12);
71
+ }
72
+ console.log(line);
73
+ if (options.verbose) {
74
+ if (assignment.description) {
75
+ const cleanDescription = assignment.description
76
+ .replace(/<[^>]*>/g, '')
77
+ .replace(/\s+/g, ' ')
78
+ .trim()
79
+ .substring(0, 150);
80
+ console.log(' ' + chalk.gray('Description: ') + cleanDescription + (cleanDescription.length === 150 ? '...' : ''));
81
+ } else {
82
+ console.log(' ' + chalk.gray('Description: N/A'));
83
+ }
84
+ console.log(' ' + chalk.gray('Submission Types: ') + (assignment.submission_types?.join(', ') || 'N/A'));
85
+ console.log(' ' + chalk.gray('Published: ') + (assignment.published ? 'Yes' : 'No'));
86
+ if (assignment.points_possible) {
87
+ console.log(' ' + chalk.gray('Points Possible: ') + assignment.points_possible);
88
+ }
89
+ if (submission && submission.attempt) {
90
+ console.log(' ' + chalk.gray('Attempt: ') + submission.attempt);
91
+ }
92
+ }
93
+ console.log('');
94
+ });
95
+ console.log(chalk.cyan('-'.repeat(60)));
96
+ } catch (error) {
97
+ console.error(chalk.red('Error fetching assignments:'), error.message);
98
+ process.exit(1);
99
+ }
100
+ }