Skip to content

Commit

Permalink
student assignment indicator, fix efficiency with assignment fetching
Browse files Browse the repository at this point in the history
  • Loading branch information
frostyfan109 committed Aug 13, 2024
1 parent aba1be2 commit 489547d
Show file tree
Hide file tree
Showing 7 changed files with 157 additions and 23 deletions.
22 changes: 22 additions & 0 deletions eduhelx_jupyterlab_student/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,27 @@ async def post(self):
self.set_status(500)
self.finish(str(e))

class NotebookFilesHandler(BaseHandler):
@tornado.web.authenticated
async def get(self):
course = await self.api.get_course()
assignments = await self.api.get_my_assignments()

assignment_notebooks = {}
for assignment in assignments:
repo_root = StudentClassRepo._compute_repo_root(course["name"]).resolve()
assignment_path = repo_root / assignment["directory_path"]

notebooks = [path.relative_to(assignment_path) for path in assignment_path.rglob("*.ipynb")]
notebooks = [path for path in notebooks if ".ipynb_checkpoints" not in path.parts]
# Sort by nestedness, then alphabetically
notebooks.sort(key=lambda path: (len(path.parents), str(path)))

assignment_notebooks[assignment["id"]] = [str(path) for path in notebooks]

self.finish(json.dumps({
"notebooks": assignment_notebooks
}))

class SettingsHandler(BaseHandler):
@tornado.web.authenticated
Expand Down Expand Up @@ -679,6 +700,7 @@ def setup_handlers(server_app):
("assignments", AssignmentsHandler),
("course_student", CourseAndStudentHandler),
("submit_assignment", SubmissionHandler),
("notebook_files", NotebookFilesHandler),
("settings", SettingsHandler)
]

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
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import React, { Fragment, useMemo } from 'react'
import React, { Fragment, useCallback, useMemo } from 'react'
import { Tooltip } from 'antd'
import { FormHelperText, Input } from '@material-ui/core'
import { ArrowBackSharp } from '@material-ui/icons'
import moment from 'moment'
import pluralize from 'pluralize'
import { ArrowBackSharp } from '@material-ui/icons'
import { assignmentInfoClass, assignmentInfoSectionClass, assignmentInfoSectionHeaderClass, assignmentInfoSectionWarningClass, assignmentNameClass, tagClass } from './style'
import { useAssignment } from '../../../contexts'
import { useAssignment, useCommands, useSnackbar } from '../../../contexts'
import { DateFormat } from '../../../utils'

const MS_IN_HOURS = 3.6e6
Expand All @@ -13,7 +14,9 @@ interface AssignmentInfoProps {
}

export const AssignmentInfo = ({ }: AssignmentInfoProps) => {
const { assignment, student, course } = useAssignment()!
const { assignment, student, course, studentNotebookExists } = useAssignment()!
const commands = useCommands()
const snackbar = useSnackbar()!
if (!student || !assignment || !course) return null

const hoursUntilDue = useMemo(() => (
Expand Down Expand Up @@ -99,6 +102,17 @@ export const AssignmentInfo = ({ }: AssignmentInfoProps) => {
else return `You are allowed to submit ${ remainingAttempts }${ assignment.currentAttempts > 0 ? ' more' : ''} ${ pluralize("time", remainingAttempts) }`
}, [assignment])

const studentNotebookInvalid = useMemo(() => (
!studentNotebookExists(assignment)
), [studentNotebookExists, assignment])

const openStudentNotebook = useCallback(async () => {
if (!commands || !assignment) return
const gradedNotebookPath = assignment.absoluteDirectoryPath + "/" + assignment.studentNotebookPath
const result = await commands.execute('docmanager:open', { path: gradedNotebookPath })
console.log(result)
}, [commands, assignment])

return (
<div className={ assignmentInfoClass }>
<div>
Expand Down Expand Up @@ -132,7 +146,7 @@ export const AssignmentInfo = ({ }: AssignmentInfoProps) => {
<span>{ course.instructors.map((ins) => ins.name).join(", ") }</span>
</div>
<div className={ assignmentInfoSectionClass }>
<h5 className={ assignmentInfoSectionHeaderClass }>Due date</h5>
<h5 className={ assignmentInfoSectionHeaderClass }>Due Date</h5>
<div>
{ assignment.isCreated ? (
new DateFormat(assignment.adjustedDueDate!).toBasicDatetime()
Expand Down Expand Up @@ -167,6 +181,31 @@ export const AssignmentInfo = ({ }: AssignmentInfoProps) => {
</div>
</div>
) }
<div className={ assignmentInfoSectionClass } style={{ marginTop: 0 }}>
<h5 className={ assignmentInfoSectionHeaderClass }>
Assignment Notebook
</h5>
<div style={{ width: "100%" }}>
{ !studentNotebookInvalid ? (
<Fragment>
<Input
readOnly
value={ assignment.studentNotebookPath }
inputProps={{ style: { width: "100%", height: 32 } }}
/>
<FormHelperText style={{ color: "#1976d2" }}>
<a onClick={ openStudentNotebook } style={{ cursor: "pointer" }}>
Open notebook
</a>
</FormHelperText>
</Fragment>
) : (
<span>
Not published yet
</span>
) }
</div>
</div>
</div>
)
}
67 changes: 58 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 @@ -40,14 +45,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 +105,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 +127,12 @@ export const AssignmentProvider = ({ fileBrowser, children }: IAssignmentProvide
setAssignments(undefined)
setCurrentAssignment(undefined)
}
setTimeout(poll, 2500)
timeoutId = window.setTimeout(poll, 2500)
}
setTimeout(poll, 2500)
poll()
return () => {
cancelled = true
window.clearTimeout(timeoutId)
}
}, [currentPath])

Expand All @@ -126,6 +141,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 +161,43 @@ export const AssignmentProvider = ({ fileBrowser, children }: IAssignmentProvide
setCourse(undefined)
setStudent(undefined)
}
setTimeout(poll, 2500)
timeoutId = window.setTimeout(poll, 2500)
}
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)
}
timeoutId = window.setTimeout(poll, 2500)
}
setTimeout(poll, 2500)
poll()
return () => {
cancelled = true
window.clearTimeout(timeoutId)
}
}, [])

Expand All @@ -160,7 +208,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

0 comments on commit 489547d

Please sign in to comment.