Skip to content

Commit

Permalink
Merge pull request #5 from helxplatform/jls-upgrades
Browse files Browse the repository at this point in the history
JLS Upgrades
  • Loading branch information
frostyfan109 authored Mar 19, 2024
2 parents 5e040a0 + 0456042 commit ce78c7f
Show file tree
Hide file tree
Showing 34 changed files with 820 additions and 2,893 deletions.
1 change: 0 additions & 1 deletion .env

This file was deleted.

14 changes: 13 additions & 1 deletion .env.sample
Original file line number Diff line number Diff line change
@@ -1 +1,13 @@
GRADER_API_URL=http://localhost:8000
# Base URL of the Grader API
GRADER_API_URL=http://localhost:8000
# User's onyen
USER_ONYEN=myonyen
# User's password
USER_AUTOGEN_PASSWORD=mypassword
# How far ahead of time to refresh the user's access token
# (proactively refreshing deals with issues such as latency and clock sync)
JWT_REFRESH_LEEWAY_SECONDS=60
# How long to keep long-polling connections alive before dropping the client (Jupyter frontend).
LONG_POLLING_TIMEOUT_SECONDS=60
# For polling that depends on unobservable data, how long to sleep in between data fetches.
LONG_POLLING_SLEEP_INTERVAL_SECONDS=5
4 changes: 4 additions & 0 deletions eduhelx_jupyterlab_student/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ class Config:
# How far ahead of time the API should refresh the access token
# (proactively refreshing using a buffer deals with issues such as latency and clock sync)
JWT_REFRESH_LEEWAY_SECONDS: int = 60
# How long to keep long-polling connections alive before dropping the client.
LONG_POLLING_TIMEOUT_SECONDS: int = 60
# For polling that depends on unobservable data, how long to sleep in between data fetches.
LONG_POLLING_SLEEP_INTERVAL_SECONDS: int = 5

"""
Map environment variables to class fields according to these rules:
Expand Down
105 changes: 85 additions & 20 deletions eduhelx_jupyterlab_student/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,19 @@
import tempfile
import shutil
import tornado
import time
import asyncio
from jupyter_server.base.handlers import APIHandler
from jupyter_server.utils import url_path_join
from pathlib import Path
from collections.abc import Iterable
from .config import ExtensionConfig
from eduhelx_utils.git import (
InvalidGitRepositoryException,
clone_repository,
get_tail_commit_id, get_repo_name, add_remote,
stage_files, commit, push, get_commit_info
stage_files, commit, push, get_commit_info,
get_modified_paths
)
from eduhelx_utils.api import Api
from .student_repo import StudentClassRepo, NotStudentClassRepositoryException
Expand Down Expand Up @@ -123,20 +127,60 @@ async def post(self):

self.finish(json.dumps(os.path.join("/", frontend_cloned_path)))

class CourseAndStudentHandler(BaseHandler):
class PollCourseStudentHandler(BaseHandler):
@tornado.web.authenticated
async def get(self):
# If this is the first poll the client has made, the `current_value` argument will not be present.
# Note: we are not deserializing `current_value`; it is a JSON-serialized string.
current_value: str | None = None
if "current_value" in self.request.arguments:
current_value = self.get_argument("current_value")

start = time.time()
while (elapsed := time.time() - start) < self.config.LONG_POLLING_TIMEOUT_SECONDS:
new_value = await CourseAndStudentHandler.get_value(self)
if new_value != current_value:
self.finish(new_value)
return
asyncio.sleep(self.config.LONG_POLLING_SLEEP_INTERVAL_SECONDS)

self.finish(new_value)

class CourseAndStudentHandler(BaseHandler):
async def get_value(self):
student = await self.api.get_my_user()
course = await self.api.get_course()
self.finish(json.dumps({
return json.dumps({
"student": student,
"course": course
}))
})

@tornado.web.authenticated
async def get(self):
self.finish(await self.get_value())

class AssignmentsHandler(BaseHandler):
class PollAssignmentsHandler(BaseHandler):
@tornado.web.authenticated
async def get(self):
current_path: str = self.get_argument("path")
# If this is the first poll the client has made, the `current_value` argument will not be present.
# Note: we are not deserializing `current_value`; it is a JSON-serialized string.
current_value: str | None = None
if "current_value" in self.request.arguments:
current_value = self.get_argument("current_value")

start = time.time()
while (elapsed := time.time() - start) < self.config.LONG_POLLING_TIMEOUT_SECONDS:
new_value = await AssignmentsHandler.get_value(self, current_path)
if new_value != current_value:
self.finish(new_value)
return
asyncio.sleep(self.config.LONG_POLLING_SLEEP_INTERVAL_SECONDS)

self.finish(new_value)

class AssignmentsHandler(BaseHandler):
async def get_value(self, current_path: str):
current_path_abs = os.path.realpath(current_path)

student = await self.api.get_my_user()
Expand All @@ -145,14 +189,13 @@ async def get(self):

value = {
"current_assignment": None,
"assignments": None
"assignments": None,
}

try:
student_repo = StudentClassRepo(course, assignments, current_path_abs)
except Exception:
self.finish(json.dumps(value))
return
return json.dumps(value)

# Add absolute path to assignment so that the frontend
# extension knows how to open the assignment without having
Expand All @@ -166,21 +209,40 @@ async def get(self):
cwd
)
# The cwd is the root in the frontend, so treat the path as such.
# NOTE: IMPORTANT: this field is NOT absolute on the server. It's only the absolute path for the webapp.
assignment["absolute_directory_path"] = os.path.join("/", rel_assignment_path)
value["assignments"] = assignments

# The student is in their repo, but we still need to check if they're actually in an assignment directory.
current_assignment = student_repo.current_assignment
if current_assignment is not None:
submissions = await self.api.get_my_submissions(current_assignment["id"])
for submission in submissions:
submission["commit"] = get_commit_info(submission["commit_id"], path=student_repo.repo_root)
current_assignment["submissions"] = submissions

value["current_assignment"] = current_assignment
self.finish(json.dumps(value))
else:
self.finish(json.dumps(value))
# The student is in their repo, but we still need to check if they're actually in an assignment directory.
if current_assignment is None:
# If user is not in an assignment, we're done. Just leave current_assignment as None.
return json.dumps(value)

submissions = await self.api.get_my_submissions(current_assignment["id"])
for submission in submissions:
submission["commit"] = get_commit_info(submission["commit_id"], path=student_repo.repo_root)
current_assignment["submissions"] = submissions
current_assignment["staged_changes"] = []
for modified_path in get_modified_paths(path=student_repo.repo_root):
full_modified_path = Path(student_repo.repo_root) / modified_path["path"]
abs_assn_path = Path(student_repo.repo_root) / assignment["directory_path"]
try:
path_relative_to_assn = full_modified_path.relative_to(abs_assn_path)
modified_path["path_from_repo"] = modified_path["path"]
modified_path["path_from_assn"] = str(path_relative_to_assn)
current_assignment["staged_changes"].append(modified_path)
except ValueError:
# This path is not part of the current assignment directory
pass

value["current_assignment"] = current_assignment
return json.dumps(value)

@tornado.web.authenticated
async def get(self):
current_path: str = self.get_argument("path")
self.finish(await self.get_value(current_path))


class SubmissionHandler(BaseHandler):
Expand Down Expand Up @@ -256,14 +318,17 @@ def setup_handlers(server_app):
base_url = web_app.settings["base_url"]
handlers = [
("assignments", AssignmentsHandler),
(("assignments", "poll"), PollAssignmentsHandler),
("course_student", CourseAndStudentHandler),
(("course_student", "poll"), PollCourseStudentHandler),
("submit_assignment", SubmissionHandler),
("clone_student_repository", CloneStudentRepositoryHandler),
("settings", SettingsHandler)
]

handlers_with_path = [
(
url_path_join(base_url, "jupyterlab-eduhelx-submission", uri),
url_path_join(base_url, "eduhelx-jupyterlab-student", *(uri if not isinstance(uri, str) else [uri])),
handler
) for (uri, handler) in handlers
]
Expand Down
Loading

0 comments on commit ce78c7f

Please sign in to comment.