From f3d0e19f46dc35a181a97591429639cfd87a9822 Mon Sep 17 00:00:00 2001 From: Riley Smith Date: Wed, 13 May 2026 17:22:44 -0700 Subject: [PATCH] fix grade submission and add user information --- AUTHORS.md | 3 +- LICENSE.md | 2 +- README.md | 2 +- src/interfaces/Responses.ts | 9 ++++++ src/services/apiService.ts | 61 ++++++++++++++++++++++++++++++++----- src/services/authService.ts | 11 +++++++ src/sidebar/classes.html | 55 +++++++++++++++++++++++++++++++++ src/sidebarProvider.ts | 20 ++++++++++-- 8 files changed, 150 insertions(+), 13 deletions(-) diff --git a/AUTHORS.md b/AUTHORS.md index ada9ae4..10ad6db 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -1,3 +1,4 @@ ## Developers + Riley Smith 2025-2026 -Sophia Kist 2024-2025 \ No newline at end of file +Sophia Kist 2024-2025 diff --git a/LICENSE.md b/LICENSE.md index e9236a7..fdddce7 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -26,4 +26,4 @@ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md index 18b7557..2dbb5fb 100644 --- a/README.md +++ b/README.md @@ -29,4 +29,4 @@ The Submitty Extension for VS Code integrates the Submitty grading system direct ## Requirements -- A valid Submitty account. \ No newline at end of file +- A valid Submitty account. diff --git a/src/interfaces/Responses.ts b/src/interfaces/Responses.ts index 6f6c2e0..390ef10 100644 --- a/src/interfaces/Responses.ts +++ b/src/interfaces/Responses.ts @@ -19,3 +19,12 @@ export type LoginResponse = ApiResponse<{ export type GradableResponse = ApiResponse<{ [key: string]: Gradable; }>; + +/** Current user from `GET /api/me` (`data` field of the envelope). */ +export interface User { + user_id: string; + user_given_name: string; + user_family_name: string; +} + +export type UserResponse = ApiResponse; diff --git a/src/services/apiService.ts b/src/services/apiService.ts index e3ef9d2..eb674ae 100644 --- a/src/services/apiService.ts +++ b/src/services/apiService.ts @@ -6,6 +6,8 @@ import { CourseResponse, LoginResponse, GradableResponse, + UserResponse, + User, } from '../interfaces/Responses'; import { AutoGraderDetails } from '../interfaces/AutoGraderDetails'; @@ -28,6 +30,7 @@ function getErrorMessage(error: unknown, fallback: string): string { export class ApiService { private client: ApiClient; private static instance: ApiService; + private currentUser: User | null = null; constructor( private context: vscode.ExtensionContext, @@ -42,6 +45,28 @@ export class ApiService { */ setAuthorizationToken(token: string): void { this.client.setToken(token); + if (!token) { + this.currentUser = null; + } + } + + getCurrentUser(): User | null { + return this.currentUser; + } + + getCurrentUserId(): string | undefined { + return this.currentUser?.user_id; + } + + /** + * Returns `user_id` from cache, or refetches `/api/me` if needed. + */ + async ensureCurrentUserId(): Promise { + if (this.currentUser?.user_id) { + return this.currentUser.user_id; + } + const user = await this.fetchMe(); + return user.user_id; } /** @@ -81,13 +106,22 @@ export class ApiService { } /** - * Fetches the current authenticated user's profile from the API. - * @returns The current user data + * Fetches the current authenticated user's profile from the API and updates the cache. + * @returns The current user (`data` object from the API envelope) */ - async fetchMe(): Promise { + async fetchMe(): Promise { try { - const response = await this.client.get('/api/me'); - return response.data; + const response = await this.client.get('/api/me'); + const body = response.data; + if ( + body?.status !== 'success' || + typeof body.data?.user_id !== 'string' || + !body.data.user_id + ) { + throw new Error('Invalid response from /api/me.'); + } + this.currentUser = body.data; + return body.data; } catch (error: unknown) { throw new Error(getErrorMessage(error, 'Failed to fetch me.'), { cause: error, @@ -205,6 +239,7 @@ export class ApiService { /** * Submits a VCS (version control) gradable to trigger autograding. + * Uses `user_id` from `/api/me` (see {@link fetchMe} / {@link ensureCurrentUserId}). * @param term - The term (e.g. "s24") * @param courseId - The course ID * @param gradeableId - The gradeable/assignment ID @@ -214,11 +249,21 @@ export class ApiService { term: string, courseId: string, gradeableId: string - ): Promise { + ): Promise { try { + const userId = await this.ensureCurrentUserId(); // git_repo_id is literally not used, but is required by the API *ugh* - const url = `/api/${term}/${courseId}/gradeable/${gradeableId}/upload?vcs_upload=true&git_repo_id=true`; - const response = await this.client.post(url); + const url = `/api/${term}/${courseId}/gradeable/${gradeableId}/grade`; + + const data = { + git_repo_id: true, + vcs_checkout: true, + user_id: userId, + }; + + const response = await this.client.post(url, data, { + headers: { 'Content-Type': 'multipart/form-data' }, + }); return response.data; } catch (error: unknown) { console.error('Error submitting VCS gradable:', error); diff --git a/src/services/authService.ts b/src/services/authService.ts index bdcde29..a8afb78 100644 --- a/src/services/authService.ts +++ b/src/services/authService.ts @@ -61,6 +61,16 @@ export class AuthService { this.apiService.setBaseUrl(baseUrl); } + try { + await this.apiService.fetchMe(); + } catch (error: unknown) { + const err = error instanceof Error ? error.message : String(error); + console.warn('Failed to load user profile (/api/me):', error); + vscode.window.showWarningMessage( + `Submitty: could not load profile (${err}). Some features may not work until you reload the window.` + ); + } + return; } @@ -140,6 +150,7 @@ export class AuthService { try { // Perform login await this.login(userId.trim(), password); + await this.apiService.fetchMe(); vscode.window.showInformationMessage( 'Successfully logged in to Submitty' diff --git a/src/sidebar/classes.html b/src/sidebar/classes.html index 23bb2c0..4e6481d 100644 --- a/src/sidebar/classes.html +++ b/src/sidebar/classes.html @@ -63,9 +63,29 @@ .homework-item:last-child { border-bottom: none; } + .profile-bar { + display: flex; + align-items: center; + margin-bottom: 16px; + padding-bottom: 12px; + border-bottom: 1px solid var(--vscode-widget-border); + } + .profile-button { + max-width: 100%; + padding: 6px 12px; + font-size: 13px; + cursor: default; + text-align: left; + background-color: var(--vscode-button-secondaryBackground); + color: var(--vscode-button-secondaryForeground); + border: 1px solid + var(--vscode-button-border, var(--vscode-contrastBorder, transparent)); + border-radius: 3px; + } +

Courses

@@ -94,9 +114,44 @@

Courses

}); } + function renderProfile(profile) { + const bar = document.getElementById('profileBar'); + if (!bar) { + return; + } + if ( + !profile || + typeof profile !== 'object' || + typeof profile.user_id !== 'string' || + !profile.user_id + ) { + bar.hidden = true; + bar.innerHTML = ''; + return; + } + var gn = + typeof profile.user_given_name === 'string' + ? profile.user_given_name + : ''; + var fn = + typeof profile.user_family_name === 'string' + ? profile.user_family_name + : ''; + var display = (gn + ' ' + fn).trim() || profile.user_id; + var title = '@' + profile.user_id; + bar.hidden = false; + bar.innerHTML = + ''; + } + window.addEventListener('message', event => { const { command, data } = event.data; if (command === 'displayCourses') { + renderProfile(data && data.profile); const courses = normalizeCourses(data || {}); const container = document.getElementById('unarchivedCourses'); if (courses.length === 0) { diff --git a/src/sidebarProvider.ts b/src/sidebarProvider.ts index 7d7d0a7..ce11b46 100644 --- a/src/sidebarProvider.ts +++ b/src/sidebarProvider.ts @@ -178,9 +178,19 @@ export class SidebarProvider implements vscode.WebviewViewProvider { }) ); + let profile = this.apiService.getCurrentUser(); + if (!profile) { + try { + await this.apiService.fetchMe(); + profile = this.apiService.getCurrentUser(); + } catch (error: unknown) { + console.warn('Could not load profile for sidebar:', error); + } + } + view.webview.postMessage({ command: MessageCommand.DISPLAY_COURSES, - data: { courses: coursesWithGradables }, + data: { courses: coursesWithGradables, profile }, }); } catch (error: unknown) { const err = error instanceof Error ? error.message : String(error); @@ -247,7 +257,13 @@ export class SidebarProvider implements vscode.WebviewViewProvider { } } - await this.apiService.submitVCSGradable(term, courseId, gradeableId); + console.log('Submitting VCS gradable...'); + const response = await this.apiService.submitVCSGradable( + term, + courseId, + gradeableId + ); + console.log('Response:', response); const gradeDetails = await vscode.window.withProgress( {