Skip to content

Commit bd686da

Browse files
committed
first pass at preloading gradeables when loading extension
1 parent 8ccb21f commit bd686da

9 files changed

Lines changed: 891 additions & 85 deletions

File tree

src/extension.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,44 @@ import { SidebarProvider } from './sidebarProvider';
33
import { ApiService } from './services/apiService';
44
import { TestingService } from './services/testingService';
55
import { GitService } from './services/gitService';
6+
import { AuthService } from './services/authService';
7+
import { CourseRepoResolver } from './services/courseRepoResolver';
8+
import type { Gradable } from './interfaces/Gradables';
69

710
export function activate(context: vscode.ExtensionContext): void {
811
const apiService = ApiService.getInstance(context, '');
912
const testingService = new TestingService(context, apiService);
1013
const gitService = new GitService();
14+
const authService = AuthService.getInstance(context);
1115
const sidebarProvider = new SidebarProvider(context, testingService, gitService);
1216

1317
context.subscriptions.push(
1418
vscode.window.registerWebviewViewProvider('submittyWebview', sidebarProvider)
1519
);
1620

21+
// Preload gradables into the Test Explorer when the workspace appears
22+
// to be a course-tied repo.
23+
void (async () => {
24+
try {
25+
await authService.initialize();
26+
const resolver = new CourseRepoResolver(apiService, authService, gitService);
27+
const courseContext = await resolver.resolveCourseContextFromRepo();
28+
if (!courseContext) {
29+
return;
30+
}
31+
32+
const gradablesResponse = await apiService.fetchGradables(courseContext.courseId, courseContext.term);
33+
const gradables = Object.values(gradablesResponse.data);
34+
35+
for (const g of gradables) {
36+
testingService.addGradeable(courseContext.term, courseContext.courseId, g.id, g.title || g.id);
37+
}
38+
} catch (e) {
39+
const err = e instanceof Error ? e.message : String(e);
40+
console.warn(`Failed to preload gradables: ${err}`);
41+
}
42+
})();
43+
1744
}
1845

1946
export function deactivate() { }

src/services/apiService.ts

Lines changed: 33 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,22 @@
22

33
import * as vscode from 'vscode';
44
import { ApiClient } from './apiClient';
5-
65
import { CourseResponse, LoginResponse, GradableResponse } from '../interfaces/Responses';
76
import { AutoGraderDetails } from '../interfaces/AutoGraderDetails';
87

8+
function getErrorMessage(error: unknown, fallback: string): string {
9+
if (error instanceof Error) {
10+
return error.message || fallback;
11+
}
12+
if (typeof error === 'object' && error) {
13+
const maybeAxiosError = error as { response?: { data?: { message?: unknown } } };
14+
const msg = maybeAxiosError.response?.data?.message;
15+
if (typeof msg === 'string' && msg.trim()) {
16+
return msg;
17+
}
18+
}
19+
return fallback;
20+
}
921

1022
export class ApiService {
1123
private client: ApiClient;
@@ -16,12 +28,12 @@ export class ApiService {
1628
}
1729

1830
// set token for local api client
19-
setAuthorizationToken(token: string) {
31+
setAuthorizationToken(token: string): void {
2032
this.client.setToken(token);
2133
}
2234

2335
// set base URL for local api client
24-
setBaseUrl(baseUrl: string) {
36+
setBaseUrl(baseUrl: string): void {
2537
this.client.setBaseURL(baseUrl);
2638
}
2739

@@ -43,31 +55,31 @@ export class ApiService {
4355

4456
const token: string = response.data.data.token;
4557
return token;
46-
} catch (error: any) {
47-
throw new Error(error.response?.data?.message || error.message || 'Login failed.');
58+
} catch (error: unknown) {
59+
throw new Error(getErrorMessage(error, 'Login failed.'));
4860
}
4961
}
5062

5163
async fetchMe(): Promise<any> {
5264
try {
5365
const response = await this.client.get<any>('/api/me');
5466
return response.data;
55-
} catch (error: any) {
56-
throw new Error(error.response?.data?.message || 'Failed to fetch me.');
67+
} catch (error: unknown) {
68+
throw new Error(getErrorMessage(error, 'Failed to fetch me.'));
5769
}
5870
}
5971

6072

6173
/**
6274
* Fetch all courses for the authenticated user
6375
*/
64-
async fetchCourses(token?: string): Promise<CourseResponse> {
76+
async fetchCourses(_token?: string): Promise<CourseResponse> {
6577
try {
6678
const response = await this.client.get<CourseResponse>('/api/courses');
6779
return response.data;
68-
} catch (error: any) {
80+
} catch (error: unknown) {
6981
console.error('Error fetching courses:', error);
70-
throw new Error(error.response?.data?.message || 'Failed to fetch courses.');
82+
throw new Error(getErrorMessage(error, 'Failed to fetch courses.'));
7183
}
7284
}
7385

@@ -76,9 +88,9 @@ export class ApiService {
7688
const url = `/api/${term}/${courseId}/gradeables`;
7789
const response = await this.client.get<GradableResponse>(url);
7890
return response.data;
79-
} catch (error: any) {
91+
} catch (error: unknown) {
8092
console.error('Error fetching gradables:', error);
81-
throw new Error(error.response?.data?.message || 'Failed to fetch gradables.');
93+
throw new Error(getErrorMessage(error, 'Failed to fetch gradables.'));
8294
}
8395
}
8496

@@ -89,9 +101,9 @@ export class ApiService {
89101
try {
90102
const response = await this.client.get<AutoGraderDetails>(`/api/${term}/${courseId}/gradeable/${gradeableId}/values`);
91103
return response.data;
92-
} catch (error: any) {
104+
} catch (error: unknown) {
93105
console.error('Error fetching grade details:', error);
94-
throw new Error(error.response?.data?.message || 'Failed to fetch grade details.');
106+
throw new Error(getErrorMessage(error, 'Failed to fetch grade details.'));
95107
}
96108
}
97109

@@ -140,24 +152,24 @@ export class ApiService {
140152
const url = `/api/${term}/${courseId}/gradeable/${gradeableId}/upload?vcs_upload=true&git_repo_id=true`;
141153
const response = await this.client.post<any>(url);
142154
return response.data;
143-
} catch (error: any) {
144-
console.error('Error submitting VCS gradable:', error);
145-
throw new Error(error.response?.data?.message || 'Failed to submit VCS gradable.');
155+
} catch (error: unknown) {
156+
console.error('Error submitt`ing VCS gradable:', error);
157+
throw new Error(getErrorMessage(error, 'Failed to submit VCS gradable.'));
146158
}
147159
}
148160

149161

150162
/**
151163
* Fetch previous attempts for a specific homework assignment
152164
*/
153-
async fetchPreviousAttempts(term: string, courseId: string, gradeableId: string): Promise<any[]> {
165+
async fetchPreviousAttempts(term: string, courseId: string, gradeableId: string): Promise<unknown[]> {
154166
try {
155167
const url = `/api/${term}/${courseId}/gradeable/${gradeableId}/attempts`;
156-
const response = await this.client.get<any>(url);
168+
const response = await this.client.get<unknown[]>(url);
157169
return response.data;
158-
} catch (error: any) {
170+
} catch (error: unknown) {
159171
console.error('Error fetching previous attempts:', error);
160-
throw new Error(error.response?.data?.message || 'Failed to fetch previous attempts.');
172+
throw new Error(getErrorMessage(error, 'Failed to fetch previous attempts.'));
161173
}
162174
}
163175

src/services/authService.ts

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ export class AuthService {
1313
this.apiService = ApiService.getInstance(context, "");
1414
}
1515

16-
async initialize() {
16+
async initialize(): Promise<void> {
1717
console.log("Initializing AuthService");
1818

1919
// Get base URL from configuration
@@ -31,6 +31,36 @@ export class AuthService {
3131
// Token exists, set it on the API service
3232
this.apiService.setAuthorizationToken(token);
3333
console.log("Token set on API service");
34+
35+
// If baseUrl isn't configured yet, fetch it now so API calls work.
36+
if (!baseUrl) {
37+
const inputUrl = await vscode.window.showInputBox({
38+
prompt: 'Enter Submitty API URL',
39+
placeHolder: 'https://example.submitty.edu',
40+
ignoreFocusOut: true,
41+
validateInput: (value) => {
42+
if (!value || value.trim().length === 0) {
43+
return 'URL is required';
44+
}
45+
try {
46+
new URL(value);
47+
return null;
48+
} catch {
49+
return 'Please enter a valid URL';
50+
}
51+
},
52+
});
53+
54+
if (!inputUrl) {
55+
return;
56+
}
57+
58+
baseUrl = inputUrl.trim();
59+
60+
await config.update('baseUrl', baseUrl, vscode.ConfigurationTarget.Global);
61+
this.apiService.setBaseUrl(baseUrl);
62+
}
63+
3464
return;
3565
}
3666

@@ -110,19 +140,20 @@ export class AuthService {
110140
await this.login(userId.trim(), password);
111141

112142
vscode.window.showInformationMessage('Successfully logged in to Submitty');
113-
} catch (error: any) {
114-
vscode.window.showErrorMessage(`Login failed: ${error.message}`);
143+
} catch (error: unknown) {
144+
const err = error instanceof Error ? error.message : String(error);
145+
vscode.window.showErrorMessage(`Login failed: ${err}`);
115146
throw error;
116147
}
117148
}
118149

119150
// store token
120-
private async storeToken(token: string) {
151+
private async storeToken(token: string): Promise<void> {
121152
await keytar.setPassword('submittyToken', 'submittyToken', token);
122153
}
123154

124155
// get token
125-
private async getToken() {
156+
private async getToken(): Promise<string | null> {
126157
return await keytar.getPassword('submittyToken', 'submittyToken');
127158
}
128159

@@ -135,7 +166,7 @@ export class AuthService {
135166
const token = await this.apiService.login(userId, password);
136167
this.apiService.setAuthorizationToken(token);
137168
// store token in system keychain
138-
this.storeToken(token);
169+
await this.storeToken(token);
139170
return token;
140171
}
141172

0 commit comments

Comments
 (0)