Skip to content

Commit

Permalink
fix
Browse files Browse the repository at this point in the history
  • Loading branch information
frostyfan109 committed Aug 14, 2024
2 parents dfd4357 + 2b2b517 commit d34ced5
Show file tree
Hide file tree
Showing 9 changed files with 311 additions and 78 deletions.
217 changes: 171 additions & 46 deletions eduhelx_jupyterlab_student/handlers.py

Large diffs are not rendered by default.

49 changes: 35 additions & 14 deletions eduhelx_jupyterlab_student/student_repo.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,40 +6,61 @@
class NotStudentClassRepositoryException(Exception):
pass


""" Note: this class is naive to the fixed repo path. It is designed for
relative interaction with class repository filepaths WHILE inside the repository. """
class StudentClassRepo:
FIXED_REPO_ROOT = "eduhelx/{}-student" # <class_name>
ORIGIN_REMOTE_NAME = "origin"
UPSTREAM_REMOTE_NAME = "upstream"
# Local branch
MAIN_BRANCH_NAME = "main"
# We have to do a merge to sync changes. We stage the merge on a separate branch
# to proactively guard against a merge conflict.
MERGE_STAGING_BRANCH_NAME = "__temp__/merge_{}-from-{}" # Formatted with the local head and upstream head commit hashes
UPSTREAM_TRACKING_BRANCH = f"{ UPSTREAM_REMOTE_NAME }/{ MAIN_BRANCH_NAME }"
ORIGIN_TRACKING_BRANCH = f"{ ORIGIN_REMOTE_NAME }/{ MAIN_BRANCH_NAME }"

def __init__(self, course, assignments, current_path):
self.course = course
self.assignments = assignments
self.current_path = os.path.realpath(current_path)

self.repo_root = self._compute_repo_root(self.course, self.current_path)
self.repo_root = self._compute_repo_root(self.course["name"], self.current_path)
self.current_assignment = self._compute_current_assignment(self.assignments, self.repo_root, self.current_path)

def get_assignment_path(self, assignment):
return os.path.join(self.repo_root, assignment["directory_path"])

@staticmethod
def _compute_repo_root(course, current_path):
try:
master_repo_remote = git.get_remote(name="upstream", path=current_path)
repo_root = os.path.realpath(
git.get_repo_root(path=current_path)
)
if master_repo_remote != course["master_remote_url"]:
def get_protected_file_paths(self, assignment) -> list[Path]:
files = []
for glob_pattern in assignment["protected_files"]:
files += self.get_assignment_path(assignment).glob(glob_pattern)
return files



@classmethod
def _compute_repo_root(cls, course_name, current_path: str | None = None):
""" Validates that user is in the repository root if current_path is provided """
repo_root = Path(cls.FIXED_REPO_ROOT.format(course_name.replace(" ", "_")))
if current_path is not None:
try:
Path(os.path.realpath(current_path)).relative_to(os.path.realpath(repo_root))
except ValueError:
raise NotStudentClassRepositoryException()
return repo_root
except InvalidGitRepositoryException as e:
raise e
return repo_root

@staticmethod
def _compute_current_assignment(assignments, repo_root, current_path):
current_assignment = None
current_path_abs = Path(current_path).resolve()
for assignment in assignments:
assignment_path = Path(os.path.join(
repo_root,
assignment["directory_path"]
))
if assignment_path == Path(current_path) or assignment_path in Path(current_path).parents:
)).resolve()
if assignment_path == current_path_abs or assignment_path in current_path_abs.parents:
current_assignment = assignment
break

Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
"@jupyterlab/coreutils": "^6.0.0",
"@jupyterlab/filebrowser": "^4.0.11",
"@jupyterlab/services": "^7.0.0",
"@jupyterlab/settingregistry": "^4.2.4",
"@jupyterlab/ui-components": "^4.0.11",
"@lumino/commands": "^2.0.0",
"@lumino/coreutils": "^2.1.2",
Expand Down
3 changes: 3 additions & 0 deletions src/api/api-responses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,11 @@ export interface AssignmentResponse {
absolute_directory_path: string
master_notebook_path: string
student_notebook_path: string
protected_files: string[]
overwritable_files: string[]
max_attempts: number | null
current_attempts: number
grader_question_feedback: boolean
created_date: string
available_date: string | null
adjusted_available_date: string | null
Expand Down
11 changes: 11 additions & 0 deletions src/api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,17 @@ export interface GetStudentAndCourseResponse {
course: ICourse
}

export interface NotebookFilesResponse {
notebooks: { [assignmentId: string]: string[] }
}

export async function listNotebookFiles(): Promise<NotebookFilesResponse> {
const data = await requestAPI<NotebookFilesResponse>(`/notebook_files`, {
method: 'GET'
})
return data
}

export async function getStudentAndCourse(): Promise<GetStudentAndCourseResponse> {
const { student, course } = await requestAPI<{
student: StudentResponse
Expand Down
12 changes: 12 additions & 0 deletions src/api/assignment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,11 @@ export interface IAssignment {
readonly absoluteDirectoryPath: string
readonly masterNotebookPath: string
readonly studentNotebookPath: string
readonly protectedFiles: string[]
readonly overwritableFiles: string[]
readonly maxAttempts: number | null
readonly currentAttempts: number
readonly graderQuestionFeedback: boolean
readonly createdDate: Date
readonly adjustedAvailableDate: Date | null
readonly adjustedDueDate: Date | null
Expand Down Expand Up @@ -51,8 +54,11 @@ export class Assignment implements IAssignment {
private _absoluteDirectoryPath: string,
private _masterNotebookPath: string,
private _studentNotebookPath: string,
private _protectedFiles: string[],
private _overwritableFiles: string[],
private _maxAttempts: number | null,
private _currentAttempts: number,
private _graderQuestionFeedback: boolean,
private _createdDate: Date,
private _adjustedAvailableDate: Date | null,
private _adjustedDueDate: Date | null,
Expand All @@ -77,8 +83,11 @@ export class Assignment implements IAssignment {
get absoluteDirectoryPath() { return this._absoluteDirectoryPath }
get masterNotebookPath() { return this._masterNotebookPath }
get studentNotebookPath() { return this._studentNotebookPath }
get protectedFiles() { return this._protectedFiles }
get overwritableFiles() { return this._overwritableFiles }
get maxAttempts() { return this._maxAttempts }
get currentAttempts() { return this._currentAttempts }
get graderQuestionFeedback() { return this._graderQuestionFeedback }
get createdDate() { return this._createdDate }
get adjustedAvailableDate() { return this._adjustedAvailableDate }
get adjustedDueDate() { return this._adjustedDueDate }
Expand Down Expand Up @@ -109,8 +118,11 @@ export class Assignment implements IAssignment {
data.absolute_directory_path,
data.master_notebook_path,
data.student_notebook_path,
data.protected_files,
data.overwritable_files,
data.max_attempts,
data.current_attempts,
data.grader_question_feedback,
new Date(data.created_date),
data.adjusted_available_date ? new Date(data.adjusted_available_date) : null,
data.adjusted_due_date ? new Date(data.adjusted_due_date) : null,
Expand Down
70 changes: 61 additions & 9 deletions src/contexts/assignment-context.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
import React, { createContext, useContext, ReactNode, useState, useMemo, useEffect } from 'react'
import React, { createContext, useContext, ReactNode, useState, useMemo, useEffect, useCallback } from 'react'
import { IChangedArgs, URLExt } from '@jupyterlab/coreutils'
import { ServerConnection } from '@jupyterlab/services'
import { FileBrowserModel, IDefaultFileBrowser } from '@jupyterlab/filebrowser'
import { showDialog, Dialog } from '@jupyterlab/apputils'
import { Button } from '@jupyterlab/ui-components'
import { useSnackbar } from './snackbar-context'
import { IEduhelxSubmissionModel } from '../tokens'
import { IAssignment, IStudent, ICurrentAssignment, ICourse, getAssignmentsPolled, GetAssignmentsResponse, getStudentAndCoursePolled, getStudentAndCourse, getAssignments } from '../api'
import { IAssignment, IStudent, ICurrentAssignment, ICourse, getAssignmentsPolled, GetAssignmentsResponse, getStudentAndCoursePolled, getStudentAndCourse, getAssignments, listNotebookFiles } from '../api'

interface StudentNotebookExists {
(assignment: IAssignment, directoryPath?: string | undefined): boolean
}

interface IAssignmentContext {
assignments: IAssignment[] | null | undefined
Expand All @@ -15,6 +19,7 @@ interface IAssignmentContext {
course: ICourse | undefined
path: string | null
loading: boolean
studentNotebookExists: StudentNotebookExists
}

interface IAssignmentProviderProps {
Expand All @@ -29,6 +34,7 @@ const WEBSOCKET_URL = URLExt.join(
"ws"
)
const WEBSOCKET_REOPEN_DELAY = 1000
const POLL_DELAY = 15000

export const AssignmentContext = createContext<IAssignmentContext|undefined>(undefined)

Expand All @@ -40,14 +46,22 @@ export const AssignmentProvider = ({ fileBrowser, children }: IAssignmentProvide
const [assignments, setAssignments] = useState<IAssignment[]|null|undefined>(undefined)
const [student, setStudent] = useState<IStudent|undefined>(undefined)
const [course, setCourse] = useState<ICourse|undefined>(undefined)
const [notebookFiles, setNotebookFiles] = useState<{ [key: string]: string[] }|undefined>(undefined)
const [ws, setWs] = useState<WebSocket>(() => new WebSocket(WEBSOCKET_URL))

const loading = useMemo(() => (
currentAssignment === undefined ||
assignments === undefined ||
student === undefined ||
course === undefined
), [currentAssignment, assignments, student, course])
course === undefined ||
notebookFiles === undefined
), [currentAssignment, assignments, student, course, notebookFiles])

const studentNotebookExists = useCallback((assignment: IAssignment, studentNotebookPath?: string | undefined) => {
if (!notebookFiles) return false
if (studentNotebookPath === undefined) studentNotebookPath = assignment.studentNotebookPath
return notebookFiles[assignment.id].some((file) => file === studentNotebookPath)
}, [notebookFiles])

useEffect(() => {
const triggerReconnect = () => {
Expand Down Expand Up @@ -92,6 +106,7 @@ export const AssignmentProvider = ({ fileBrowser, children }: IAssignmentProvide
setCurrentAssignment(undefined)

let cancelled = false
let timeoutId: number | undefined = undefined
const poll = async () => {
let value
if (currentPath !== null) {
Expand All @@ -113,11 +128,12 @@ export const AssignmentProvider = ({ fileBrowser, children }: IAssignmentProvide
setAssignments(undefined)
setCurrentAssignment(undefined)
}
setTimeout(poll, 2500)
timeoutId = window.setTimeout(poll, POLL_DELAY)
}
setTimeout(poll, 2500)
poll()
return () => {
cancelled = true
window.clearTimeout(timeoutId)
}
}, [currentPath])

Expand All @@ -126,6 +142,7 @@ export const AssignmentProvider = ({ fileBrowser, children }: IAssignmentProvide
setStudent(undefined)

let cancelled = false
let timeoutId: number | undefined = undefined
const poll = async () => {
let value
try {
Expand All @@ -145,11 +162,45 @@ export const AssignmentProvider = ({ fileBrowser, children }: IAssignmentProvide
setCourse(undefined)
setStudent(undefined)
}
setTimeout(poll, 2500)
timeoutId = window.setTimeout(poll, POLL_DELAY)
}
poll()
return () => {
cancelled = true
window.clearTimeout(timeoutId)
}
}, [])

useEffect(() => {
setNotebookFiles(undefined)

let cancelled = false
let timeoutId: number | undefined = undefined
const poll = async () => {
let value
try {
value = await listNotebookFiles()
} catch (e: any) {
console.error(e)
snackbar.open({
type: 'warning',
message: 'Failed to pull course data...'
})
}
if (cancelled) return
if (value !== undefined) {
setNotebookFiles(value.notebooks)
} else {
setNotebookFiles(undefined)
}
// We don't use POLL_DELAY for fetching notebook files, since this needs to be reflected more rapidly
// to the user and also doesn't involve any API calls, only scanning the directory for ipynb files.
timeoutId = window.setTimeout(poll, 2500)
}
setTimeout(poll, 2500)
poll()
return () => {
cancelled = true
window.clearTimeout(timeoutId)
}
}, [])

Expand All @@ -160,7 +211,8 @@ export const AssignmentProvider = ({ fileBrowser, children }: IAssignmentProvide
student,
course,
path: currentPath,
loading
loading,
studentNotebookExists
}}>
{ children }
</AssignmentContext.Provider>
Expand Down
16 changes: 7 additions & 9 deletions src/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,18 +27,16 @@ export async function requestAPI<T>(
throw new ServerConnection.NetworkError(error as any)
}

let data: any = await response.text()

if (data.length > 0) {
try {
data = JSON.parse(data)
} catch (error) {
console.log('Not a JSON response body.', response)
}
let data
try {
// Clone so we don't read the response body in the event of an error (we return the response).
data = await response.clone().json()
} catch (error) {
console.log('Not a JSON response body.', response)
}

if (!response.ok) {
throw new ServerConnection.ResponseError(response, data.message || data)
throw new ServerConnection.ResponseError(response, data?.message || data)
}

return data
Expand Down
10 changes: 10 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,14 @@ import { AssignmentWidget } from './widgets'
import { EduhelxSubmissionModel } from './model'
import { submissionIcon } from './style/icons'
import { IFileBrowserFactory } from '@jupyterlab/filebrowser'
import { ISettingRegistry } from '@jupyterlab/settingregistry'

async function activate (
app: JupyterFrontEnd,
fileBrowser: IDefaultFileBrowser,
restorer: ILayoutRestorer,
shell: ILabShell,
settingRegistry: ISettingRegistry
) {
let serverSettings: IServerSettings
try {
Expand All @@ -32,6 +34,13 @@ async function activate (
return
}

Promise.all([app.restored, fileBrowser.model.restored]).then(() => {
// A couple things are required to get jupyter to show dotfiles/~ files/etc.
// This browser setting needs to be enabled for filebrowser, as well as
// the server setting ContentsManager.allow_hidden=True
settingRegistry.set("@jupyterlab/filebrowser-extension:browser", "showHiddenFiles", true)
})

// const model = new EduhelxSubmissionModel()
// Promise.all([app.restored, fileBrowser.model.restored]).then(() => {
// model.currentPath = fileBrowser.model.path
Expand Down Expand Up @@ -61,6 +70,7 @@ const plugin: JupyterFrontEndPlugin<void> = {
IDefaultFileBrowser,
ILayoutRestorer,
ILabShell,
ISettingRegistry
],
activate
};
Expand Down

0 comments on commit d34ced5

Please sign in to comment.