From 2a0ae8a23f50335c77aaec21e7c7054cdd1a3352 Mon Sep 17 00:00:00 2001
From: DerGrumpf
Date: Mon, 15 Sep 2025 22:13:37 +0200
Subject: [PATCH] Added: NbGrader as external service
---
.gitignore | 1 +
jupyterhub/compose.yml | 5 +--
jupyterhub/jupyterhub_config.py | 64 +++++++++++++++++++++++++++++++--
jupyterhub/nbgrader_config.py | 56 ++++++++++++++++++-----------
4 files changed, 101 insertions(+), 25 deletions(-)
diff --git a/.gitignore b/.gitignore
index a157c00..7427c59 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,7 @@
# --- This Project ---#
data/
logs/
+nbgrader/
.env
# Docker secrets
diff --git a/jupyterhub/compose.yml b/jupyterhub/compose.yml
index 89cc6f3..ca66909 100644
--- a/jupyterhub/compose.yml
+++ b/jupyterhub/compose.yml
@@ -11,5 +11,6 @@ services:
- /var/run/docker.sock:/var/run/docker.sock
- ./templates:/srv/jupyterhub/templates:ro
- ./jupyterhub_config.py:/srv/jupyterhub/jupyterhub_config.py
- - ./nbgrader_config.py:/srv/nbgrader/courses/nbgrader_config.py
- - ../logs/jupyterhub:/var/log/jupyterhub
+ - ./nbgrader_config.py:/srv/nbgrader/nbgrader_config.py:ro
+ - ../nbgrader:/srv/nbgrade
+ - ../nbgrader:/srv/nbgrader
diff --git a/jupyterhub/jupyterhub_config.py b/jupyterhub/jupyterhub_config.py
index 2252c8d..581a60e 100644
--- a/jupyterhub/jupyterhub_config.py
+++ b/jupyterhub/jupyterhub_config.py
@@ -86,11 +86,14 @@ c.JupyterHub.template_paths = ["/etc/jupyterhub/templates"]
c.JupyterHub.spawner_class = DockerSpawner
# Spawn Containers from this Image
-c.DockerSpawner.image = os.environ.get("NOTEBOOK_IMAGE", "jupyter/base-notebook:latest")
+c.DockerSpawner.image = os.environ.get(
+ "NOTEBOOK_IMAGE", "jupyter/datascience-notebook:latest"
+)
# Connect Containers to this network
network_name = os.environ.get("DOCKER_NETWORK_NAME", "stack_default")
-c.DockerSpawner.use_internal_ip = True # Let docker manage network communications
+# Let docker manage network communications
+c.DockerSpawner.use_internal_ip = True
c.DockerSpawner.network_name = network_name
# Explicitly set notebook directory because we'll mounting a volume to it.
@@ -128,6 +131,10 @@ c.Spawner.args = [
"--NotebookApp.allow_credentials=True",
"--NotebookApp.disable_check_xsrf=True",
'--ServerApp.tornado_settings={"headers": {"Access-Control-Allow-Origin": "*"}}',
+ # NBgrader extensions
+ "--NotebookApp.nbserver_extensions={'nbgrader.server_extensions.formgrader':True}",
+ "--NotebookApp.nbserver_extensions={'nbgrader.server_extensions.assignment_list':True}",
+ "--NotebookApp.nbserver_extensions={'nbgrader.server_extensions.course_list':True}",
]
# Security settings
@@ -148,3 +155,56 @@ c.JupyterHub.concurrent_spawn_limit = 10
# Allow named servers
c.JupyterHub.allow_named_servers = True
c.JupyterHub.named_server_limit_per_user = 3
+
+
+# NBgrader
+c.JupyterHub.services = [
+ {
+ "name": "nbgrader-formgrader",
+ "url": "http://127.0.0.1:9999",
+ "command": [
+ "python",
+ "-m",
+ "nbgrader.apps.formgraderapp",
+ "--FormgradeApp.authenticator_plugin_class=nbgrader.auth.JupyterHubAuthPlugin",
+ "--FormgradeApp.ip=0.0.0.0",
+ "--FormgradeApp.port=9999",
+ f"--FormgradeApp.base_url={base_url}services/nbgrader-formgrader/",
+ ],
+ "cwd": "/srv/nbgrader/exchange",
+ "environment": {
+ "JUPYTERHUB_API_TOKEN": "", # Will be set automatically
+ "JUPYTERHUB_API_URL": f"http://jupyterhub:{port}{base_url}hub/api",
+ "JUPYTERHUB_SERVICE_PREFIX": "/services/nbgrader-formgrader/",
+ "POSTGRES_USER": p_user,
+ "POSTGRES_PASSWORD": p_pass,
+ "POSTGRES_HOST": p_host,
+ "POSTGRES_DB": p_db,
+ },
+ }
+]
+
+nbgrader_exchange_dir = "/srv/nbgrader/exchange"
+
+c.DockerSpawner.environment = {
+ "JUPYTERHUB_API_TOKEN": "", # Will be populated automatically
+ "JUPYTERHUB_API_URL": f"http://jupyterhub:{port}{base_url}hub/api",
+ "NBGRADER_EXCHANGE_DIRECTORY": nbgrader_exchange_dir,
+ "NBGRADER_CACHE_DIRECTORY": "/home/jovyan/.cache/nbgrader",
+ "NBGRADER_CONFIG_FILE": "/home/jovyan/.jupyter/nbgrader_config.py",
+}
+
+# Configure NBgrader to use the same PostgreSQL database
+c.NbGrader.db_url = f"postgresql://{p_user}:{p_pass}@{p_host}:5432/{p_db}"
+
+# NBgrader course configuration
+c.CourseDirectory.root = "/srv/nbgrader"
+c.CourseDirectory.course_id = os.environ.get("NBGRADER_COURSE_ID", "default_course")
+
+# Exchange configuration
+c.Exchange.root = nbgrader_exchange_dir
+c.Exchange.course_id = os.environ.get("NBGRADER_COURSE_ID", "default_course")
+
+# Formgrader configuration
+c.FormgradeApp.authenticator_plugin_class = "nbgrader.auth.JupyterHubAuthPlugin"
+c.FormgradeApp.base_url = f"{base_url}services/nbgrader-formgrader/"
diff --git a/jupyterhub/nbgrader_config.py b/jupyterhub/nbgrader_config.py
index 0e8e798..54a28d8 100644
--- a/jupyterhub/nbgrader_config.py
+++ b/jupyterhub/nbgrader_config.py
@@ -1,30 +1,44 @@
-# /srv/nbgrader/nbgrader_config.py
+import os
+
c = get_config()
-# 1. Course Configuration
-c.CourseDirectory.course_id = "intro-to-python" # Change to your course name
-c.CourseDirectory.root = '/srv/nbgrader/courses'
+# Database configuration - use the same PostgreSQL as JupyterHub
+p_user = os.environ.get("POSTGRES_USER", "")
+p_pass = os.environ.get("POSTGRES_PASSWORD", "")
+p_host = os.environ.get("POSTGRES_HOST", "postgres")
+p_db = os.environ.get("POSTGRES_DB", "jupyterhub")
-# 2. Exchange Directory (for submitting/collecting assignments)
-c.Exchange.root = '/srv/nbgrader/exchange'
-c.Exchange.path_includes_course = True
+# NBgrader database URL
+c.NbGrader.db_url = f"postgresql://{p_user}:{p_pass}@{p_host}:5432/{p_db}"
-# 3. JupyterHub Integration
-#c.NbGrader.hub_url = 'http://jupyterhub:8081/hub/api'
-#c.NbGrader.hubapi_token = 'your-hub-token' # Generate with: openssl rand -hex 32
+# Course configuration
+course_id = os.environ.get("NBGRADER_COURSE_ID", "default_course")
+c.CourseDirectory.course_id = course_id
+c.CourseDirectory.root = "/srv/nbgrader"
-# 4. Database Configuration
-c.StudentAssignmentNotebook.database_url = 'sqlite:////srv/nbgrader/nbgrader.sqlite'
+# Exchange directory configuration
+exchange_dir = os.environ.get("NBGRADER_EXCHANGE_DIRECTORY", "/srv/nbgrader/exchange")
+c.Exchange.root = exchange_dir
+c.Exchange.course_id = course_id
-# 5. Assignment Policies
-c.ExecutePreprocessor.timeout = 300 # 5 minutes timeout per cell
-c.ClearSolutions.code_stub = "# YOUR CODE HERE"
-c.ClearSolutions.text_stub = "YOUR ANSWER HERE"
+# Authentication
+c.FormgradeApp.authenticator_plugin_class = "nbgrader.auth.JupyterHubAuthPlugin"
-# 6. Formgrader UI Settings
+# Logging
+c.Application.log_level = "INFO"
+
+# Formgrader settings
+c.FormgradeApp.ip = "0.0.0.0"
c.FormgradeApp.port = 9999
-c.FormgradeApp.authenticator_class = 'nbgrader.auth.hubauth.HubAuth'
-c.FormgradeApp.ip = '0.0.0.0'
+c.FormgradeApp.base_url = "/jupyter/services/nbgrader-formgrader/"
-# 7. Permissions
-c.CourseDirectory.groupshared = True
+# Enable tornado debug mode for development
+# c.FormgradeApp.tornado_settings = {'debug': True}
+
+# Assignment settings
+c.AssignmentListApp.assignment_dir = "/home/jovyan/assignments"
+c.AssignmentListApp.exchange_directory = exchange_dir
+
+# Grader settings
+c.AutogradeApp.force = True
+c.GenerateAssignmentApp.force = True