e2x-course-hub 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- e2x_course_hub-0.2.0/.github/workflows/ci.yml +97 -0
- e2x_course_hub-0.2.0/.github/workflows/python-publish.yaml +53 -0
- e2x_course_hub-0.2.0/.gitignore +143 -0
- e2x_course_hub-0.2.0/LICENSE +21 -0
- e2x_course_hub-0.2.0/PKG-INFO +269 -0
- e2x_course_hub-0.2.0/README.md +228 -0
- e2x_course_hub-0.2.0/course-service-ui/.env.development +4 -0
- e2x_course_hub-0.2.0/course-service-ui/.gitignore +24 -0
- e2x_course_hub-0.2.0/course-service-ui/.prettierignore +3 -0
- e2x_course_hub-0.2.0/course-service-ui/eslint.config.js +23 -0
- e2x_course_hub-0.2.0/course-service-ui/package-lock.json +4937 -0
- e2x_course_hub-0.2.0/course-service-ui/package.json +53 -0
- e2x_course_hub-0.2.0/course-service-ui/public/favicon.ico +0 -0
- e2x_course_hub-0.2.0/course-service-ui/src/App.tsx +21 -0
- e2x_course_hub-0.2.0/course-service-ui/src/api/client.ts +130 -0
- e2x_course_hub-0.2.0/course-service-ui/src/api/courses.ts +105 -0
- e2x_course_hub-0.2.0/course-service-ui/src/api/http.ts +25 -0
- e2x_course_hub-0.2.0/course-service-ui/src/api/index.ts +12 -0
- e2x_course_hub-0.2.0/course-service-ui/src/api/infrastructure.ts +29 -0
- e2x_course_hub-0.2.0/course-service-ui/src/api/me.ts +12 -0
- e2x_course_hub-0.2.0/course-service-ui/src/api/membership.ts +118 -0
- e2x_course_hub-0.2.0/course-service-ui/src/api/roles.ts +14 -0
- e2x_course_hub-0.2.0/course-service-ui/src/api/schema.d.ts +1789 -0
- e2x_course_hub-0.2.0/course-service-ui/src/api/types.ts +147 -0
- e2x_course_hub-0.2.0/course-service-ui/src/components/actions/ActionList.tsx +162 -0
- e2x_course_hub-0.2.0/course-service-ui/src/components/actions/RoleActionsModal.tsx +190 -0
- e2x_course_hub-0.2.0/course-service-ui/src/components/actions/RoleInfoLink.tsx +34 -0
- e2x_course_hub-0.2.0/course-service-ui/src/components/actions/YourAccessButton.tsx +46 -0
- e2x_course_hub-0.2.0/course-service-ui/src/components/course/CoursePage.tsx +81 -0
- e2x_course_hub-0.2.0/course-service-ui/src/components/course/tabs/CourseOverviewTab.tsx +165 -0
- e2x_course_hub-0.2.0/course-service-ui/src/components/course/tabs/CourseOwnersTab.tsx +34 -0
- e2x_course_hub-0.2.0/course-service-ui/src/components/course/tabs/CourseSettingsTab.tsx +179 -0
- e2x_course_hub-0.2.0/course-service-ui/src/components/courses/CoursesPage.tsx +61 -0
- e2x_course_hub-0.2.0/course-service-ui/src/components/courses/tabs/CourseCreatorsTab.tsx +29 -0
- e2x_course_hub-0.2.0/course-service-ui/src/components/courses/tabs/CourseListTab.tsx +140 -0
- e2x_course_hub-0.2.0/course-service-ui/src/components/courses/tabs/LMSAdminsTab.tsx +29 -0
- e2x_course_hub-0.2.0/course-service-ui/src/components/dialogs/CreateCourseDialog.tsx +137 -0
- e2x_course_hub-0.2.0/course-service-ui/src/components/dialogs/CreateTermDialog.tsx +95 -0
- e2x_course_hub-0.2.0/course-service-ui/src/components/dialogs/ImageSelectionDialog.tsx +113 -0
- e2x_course_hub-0.2.0/course-service-ui/src/components/dialogs/ResourceSelectionDialog.tsx +93 -0
- e2x_course_hub-0.2.0/course-service-ui/src/components/infrastructure/RuntimeConfigEditor.tsx +156 -0
- e2x_course_hub-0.2.0/course-service-ui/src/components/membership/AddMembersDialog.tsx +93 -0
- e2x_course_hub-0.2.0/course-service-ui/src/components/membership/MemberDataTable.tsx +185 -0
- e2x_course_hub-0.2.0/course-service-ui/src/components/membership/MemberDataTableWithCurrentUser.tsx +12 -0
- e2x_course_hub-0.2.0/course-service-ui/src/components/membership/MemberListCard.tsx +118 -0
- e2x_course_hub-0.2.0/course-service-ui/src/components/membership/TabbedMemberDataTable.tsx +107 -0
- e2x_course_hub-0.2.0/course-service-ui/src/components/membership/TabbedMemberDataTableWithCurrentUser.tsx +11 -0
- e2x_course_hub-0.2.0/course-service-ui/src/components/term/TermPage.tsx +81 -0
- e2x_course_hub-0.2.0/course-service-ui/src/components/term/tabs/TermOverviewTab.tsx +252 -0
- e2x_course_hub-0.2.0/course-service-ui/src/components/term/tabs/TermSettingsTab.tsx +95 -0
- e2x_course_hub-0.2.0/course-service-ui/src/components/ui/Alert.tsx +35 -0
- e2x_course_hub-0.2.0/course-service-ui/src/components/ui/Badge.tsx +20 -0
- e2x_course_hub-0.2.0/course-service-ui/src/components/ui/Button.tsx +30 -0
- e2x_course_hub-0.2.0/course-service-ui/src/components/ui/Card.tsx +24 -0
- e2x_course_hub-0.2.0/course-service-ui/src/components/ui/ConfirmDialog.tsx +132 -0
- e2x_course_hub-0.2.0/course-service-ui/src/components/ui/DataTable.tsx +322 -0
- e2x_course_hub-0.2.0/course-service-ui/src/components/ui/Modal.tsx +71 -0
- e2x_course_hub-0.2.0/course-service-ui/src/components/ui/Row.tsx +19 -0
- e2x_course_hub-0.2.0/course-service-ui/src/components/ui/SelectionBox.tsx +48 -0
- e2x_course_hub-0.2.0/course-service-ui/src/components/ui/Switch.tsx +33 -0
- e2x_course_hub-0.2.0/course-service-ui/src/components/ui/TabBar.tsx +33 -0
- e2x_course_hub-0.2.0/course-service-ui/src/components/ui/Tabs.tsx +52 -0
- e2x_course_hub-0.2.0/course-service-ui/src/config.ts +27 -0
- e2x_course_hub-0.2.0/course-service-ui/src/domain/actions/courses.ts +97 -0
- e2x_course_hub-0.2.0/course-service-ui/src/domain/actions/index.ts +5 -0
- e2x_course_hub-0.2.0/course-service-ui/src/domain/actions/membership.ts +42 -0
- e2x_course_hub-0.2.0/course-service-ui/src/domain/actions/terms.ts +47 -0
- e2x_course_hub-0.2.0/course-service-ui/src/domain/actions/tree.ts +6 -0
- e2x_course_hub-0.2.0/course-service-ui/src/domain/actions/types.ts +17 -0
- e2x_course_hub-0.2.0/course-service-ui/src/domain/roles.ts +39 -0
- e2x_course_hub-0.2.0/course-service-ui/src/hooks/catalog/index.ts +2 -0
- e2x_course_hub-0.2.0/course-service-ui/src/hooks/catalog/keys.ts +6 -0
- e2x_course_hub-0.2.0/course-service-ui/src/hooks/catalog/queries.ts +29 -0
- e2x_course_hub-0.2.0/course-service-ui/src/hooks/course/index.ts +3 -0
- e2x_course_hub-0.2.0/course-service-ui/src/hooks/course/keys.ts +12 -0
- e2x_course_hub-0.2.0/course-service-ui/src/hooks/course/mutations.ts +130 -0
- e2x_course_hub-0.2.0/course-service-ui/src/hooks/course/queries.ts +67 -0
- e2x_course_hub-0.2.0/course-service-ui/src/hooks/index.ts +0 -0
- e2x_course_hub-0.2.0/course-service-ui/src/hooks/me/index.ts +1 -0
- e2x_course_hub-0.2.0/course-service-ui/src/hooks/me/queries.ts +10 -0
- e2x_course_hub-0.2.0/course-service-ui/src/hooks/membership/index.ts +3 -0
- e2x_course_hub-0.2.0/course-service-ui/src/hooks/membership/keys.ts +54 -0
- e2x_course_hub-0.2.0/course-service-ui/src/hooks/membership/mutations.ts +161 -0
- e2x_course_hub-0.2.0/course-service-ui/src/hooks/membership/queries.ts +111 -0
- e2x_course_hub-0.2.0/course-service-ui/src/hooks/roles/index.ts +1 -0
- e2x_course_hub-0.2.0/course-service-ui/src/hooks/roles/queries.ts +16 -0
- e2x_course_hub-0.2.0/course-service-ui/src/hooks/ui/index.ts +2 -0
- e2x_course_hub-0.2.0/course-service-ui/src/hooks/ui/useBodyScrollLock.ts +17 -0
- e2x_course_hub-0.2.0/course-service-ui/src/hooks/ui/useSelection.ts +56 -0
- e2x_course_hub-0.2.0/course-service-ui/src/index.css +34 -0
- e2x_course_hub-0.2.0/course-service-ui/src/lib/buildDefaultCourseConfig.ts +59 -0
- e2x_course_hub-0.2.0/course-service-ui/src/lib/dataTablePageSize.ts +15 -0
- e2x_course_hub-0.2.0/course-service-ui/src/lib/errorMessage.ts +28 -0
- e2x_course_hub-0.2.0/course-service-ui/src/main.tsx +18 -0
- e2x_course_hub-0.2.0/course-service-ui/src/queryClient.ts +3 -0
- e2x_course_hub-0.2.0/course-service-ui/tsconfig.app.json +39 -0
- e2x_course_hub-0.2.0/course-service-ui/tsconfig.json +18 -0
- e2x_course_hub-0.2.0/course-service-ui/tsconfig.node.json +26 -0
- e2x_course_hub-0.2.0/course-service-ui/vite.config.ts +42 -0
- e2x_course_hub-0.2.0/docs/Makefile +21 -0
- e2x_course_hub-0.2.0/docs/make.bat +35 -0
- e2x_course_hub-0.2.0/docs/requirements.txt +6 -0
- e2x_course_hub-0.2.0/docs/source/conf.py +53 -0
- e2x_course_hub-0.2.0/docs/source/course_service/overview.md +85 -0
- e2x_course_hub-0.2.0/docs/source/getting_started/installation.md +24 -0
- e2x_course_hub-0.2.0/docs/source/images/course_overview.png +0 -0
- e2x_course_hub-0.2.0/docs/source/images/course_select_notebook_image.png +0 -0
- e2x_course_hub-0.2.0/docs/source/images/course_select_student_resources.png +0 -0
- e2x_course_hub-0.2.0/docs/source/images/course_service_courses.png +0 -0
- e2x_course_hub-0.2.0/docs/source/images/term_add_students.png +0 -0
- e2x_course_hub-0.2.0/docs/source/images/term_overview.png +0 -0
- e2x_course_hub-0.2.0/docs/source/images/term_what_can_a_student_do.png +0 -0
- e2x_course_hub-0.2.0/docs/source/index.md +62 -0
- e2x_course_hub-0.2.0/docs/source/reference/configuration.md +52 -0
- e2x_course_hub-0.2.0/docs/source/reference/rest_api.md +45 -0
- e2x_course_hub-0.2.0/docs/source/reference/roles.md +86 -0
- e2x_course_hub-0.2.0/docs/source/spawner/contract.md +39 -0
- e2x_course_hub-0.2.0/docs/source/user_guide/courses.md +85 -0
- e2x_course_hub-0.2.0/docs/source/user_guide/members.md +51 -0
- e2x_course_hub-0.2.0/docs/source/user_guide/semesters.md +56 -0
- e2x_course_hub-0.2.0/e2x_course_hub/__about__.py +1 -0
- e2x_course_hub-0.2.0/e2x_course_hub/__init__.py +0 -0
- e2x_course_hub-0.2.0/e2x_course_hub/api/__init__.py +6 -0
- e2x_course_hub-0.2.0/e2x_course_hub/api/api.py +42 -0
- e2x_course_hub-0.2.0/e2x_course_hub/api/course_api.py +536 -0
- e2x_course_hub-0.2.0/e2x_course_hub/api/course_permissions.py +101 -0
- e2x_course_hub-0.2.0/e2x_course_hub/api/infrastructure_api.py +43 -0
- e2x_course_hub-0.2.0/e2x_course_hub/api/infrastructure_permissions.py +51 -0
- e2x_course_hub-0.2.0/e2x_course_hub/api/membership_api.py +47 -0
- e2x_course_hub-0.2.0/e2x_course_hub/api/role_permissions.py +27 -0
- e2x_course_hub-0.2.0/e2x_course_hub/api/spawn_offering_api.py +60 -0
- e2x_course_hub-0.2.0/e2x_course_hub/api/spawn_permissions.py +70 -0
- e2x_course_hub-0.2.0/e2x_course_hub/contract/README.md +143 -0
- e2x_course_hub-0.2.0/e2x_course_hub/contract/__init__.py +46 -0
- e2x_course_hub-0.2.0/e2x_course_hub/contract/providers.py +27 -0
- e2x_course_hub-0.2.0/e2x_course_hub/course_service/__init__.py +0 -0
- e2x_course_hub-0.2.0/e2x_course_hub/course_service/_data.py +25 -0
- e2x_course_hub-0.2.0/e2x_course_hub/course_service/actions/__init__.py +23 -0
- e2x_course_hub-0.2.0/e2x_course_hub/course_service/actions/builders.py +149 -0
- e2x_course_hub-0.2.0/e2x_course_hub/course_service/actions/can.py +34 -0
- e2x_course_hub-0.2.0/e2x_course_hub/course_service/actions/models.py +93 -0
- e2x_course_hub-0.2.0/e2x_course_hub/course_service/common/__init__.py +0 -0
- e2x_course_hub-0.2.0/e2x_course_hub/course_service/common/dependencies.py +56 -0
- e2x_course_hub-0.2.0/e2x_course_hub/course_service/common/dependency_types.py +24 -0
- e2x_course_hub-0.2.0/e2x_course_hub/course_service/common/schemas.py +20 -0
- e2x_course_hub-0.2.0/e2x_course_hub/course_service/courses/__init__.py +0 -0
- e2x_course_hub-0.2.0/e2x_course_hub/course_service/courses/assembler.py +102 -0
- e2x_course_hub-0.2.0/e2x_course_hub/course_service/courses/dependencies.py +45 -0
- e2x_course_hub-0.2.0/e2x_course_hub/course_service/courses/router.py +131 -0
- e2x_course_hub-0.2.0/e2x_course_hub/course_service/courses/schemas.py +30 -0
- e2x_course_hub-0.2.0/e2x_course_hub/course_service/exception_handlers.py +21 -0
- e2x_course_hub-0.2.0/e2x_course_hub/course_service/fastapi_app.py +100 -0
- e2x_course_hub-0.2.0/e2x_course_hub/course_service/infrastructure/__init__.py +0 -0
- e2x_course_hub-0.2.0/e2x_course_hub/course_service/infrastructure/assembler.py +37 -0
- e2x_course_hub-0.2.0/e2x_course_hub/course_service/infrastructure/dependencies.py +17 -0
- e2x_course_hub-0.2.0/e2x_course_hub/course_service/infrastructure/router.py +35 -0
- e2x_course_hub-0.2.0/e2x_course_hub/course_service/infrastructure/schemas.py +20 -0
- e2x_course_hub-0.2.0/e2x_course_hub/course_service/me/__init__.py +0 -0
- e2x_course_hub-0.2.0/e2x_course_hub/course_service/me/router.py +22 -0
- e2x_course_hub-0.2.0/e2x_course_hub/course_service/me/schemas.py +5 -0
- e2x_course_hub-0.2.0/e2x_course_hub/course_service/membership/__init__.py +0 -0
- e2x_course_hub-0.2.0/e2x_course_hub/course_service/membership/router.py +184 -0
- e2x_course_hub-0.2.0/e2x_course_hub/course_service/membership/schemas.py +15 -0
- e2x_course_hub-0.2.0/e2x_course_hub/course_service/roles/__init__.py +0 -0
- e2x_course_hub-0.2.0/e2x_course_hub/course_service/roles/router.py +26 -0
- e2x_course_hub-0.2.0/e2x_course_hub/course_service/roles/schemas.py +24 -0
- e2x_course_hub-0.2.0/e2x_course_hub/course_service/settings.py +11 -0
- e2x_course_hub-0.2.0/e2x_course_hub/course_service/terms/__init__.py +0 -0
- e2x_course_hub-0.2.0/e2x_course_hub/course_service/terms/assembler.py +67 -0
- e2x_course_hub-0.2.0/e2x_course_hub/course_service/terms/dependencies.py +38 -0
- e2x_course_hub-0.2.0/e2x_course_hub/course_service/terms/router.py +83 -0
- e2x_course_hub-0.2.0/e2x_course_hub/course_service/terms/schemas.py +24 -0
- e2x_course_hub-0.2.0/e2x_course_hub/course_service/whoami.py +40 -0
- e2x_course_hub-0.2.0/e2x_course_hub/db/__init__.py +7 -0
- e2x_course_hub-0.2.0/e2x_course_hub/db/database.py +53 -0
- e2x_course_hub-0.2.0/e2x_course_hub/db/models.py +106 -0
- e2x_course_hub-0.2.0/e2x_course_hub/db/repository.py +423 -0
- e2x_course_hub-0.2.0/e2x_course_hub/errors.py +187 -0
- e2x_course_hub-0.2.0/e2x_course_hub/loader.py +110 -0
- e2x_course_hub-0.2.0/e2x_course_hub/schema/catalog.py +211 -0
- e2x_course_hub-0.2.0/e2x_course_hub/schema/course.py +58 -0
- e2x_course_hub-0.2.0/e2x_course_hub/schema/selection.py +80 -0
- e2x_course_hub-0.2.0/e2x_course_hub/schema/types.py +20 -0
- e2x_course_hub-0.2.0/e2x_course_hub/settings.py +37 -0
- e2x_course_hub-0.2.0/e2x_course_hub/utils.py +64 -0
- e2x_course_hub-0.2.0/openapi.json +2470 -0
- e2x_course_hub-0.2.0/pyproject.toml +124 -0
- e2x_course_hub-0.2.0/scripts/export_openapi.py +70 -0
- e2x_course_hub-0.2.0/share/e2x_course_hub/static/course-service-ui/assets/main.css +1 -0
- e2x_course_hub-0.2.0/share/e2x_course_hub/static/course-service-ui/favicon.ico +0 -0
- e2x_course_hub-0.2.0/share/e2x_course_hub/static/course-service-ui/main.js +20 -0
- e2x_course_hub-0.2.0/share/e2x_course_hub/static/course-service-ui/main.js.map +1 -0
- e2x_course_hub-0.2.0/share/e2x_course_hub/templates/course_service/react_base.j2 +21 -0
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
pull_request:
|
|
6
|
+
|
|
7
|
+
concurrency:
|
|
8
|
+
group: ${{ github.workflow }}-${{ github.ref }}
|
|
9
|
+
cancel-in-progress: true
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
python:
|
|
13
|
+
name: Python lint
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v7
|
|
17
|
+
|
|
18
|
+
# Ruff reads its config from pyproject.toml and never imports the package,
|
|
19
|
+
# so this job deliberately skips installing the project — doing so would
|
|
20
|
+
# drag in Node just to build the UI bundle (see the frontend job).
|
|
21
|
+
#
|
|
22
|
+
# The action installs the ruff version pinned in pyproject.toml's `dev`
|
|
23
|
+
# extra. That pin matters: formatter output changes between releases, so
|
|
24
|
+
# an unpinned ruff makes CI disagree with developers' local ruff without
|
|
25
|
+
# anything in the repository having changed.
|
|
26
|
+
- name: Check formatting
|
|
27
|
+
uses: astral-sh/ruff-action@278981a28ce3188b1e39527901f38254bf3aac89 # v4.1.0
|
|
28
|
+
with:
|
|
29
|
+
args: "format --check --diff"
|
|
30
|
+
|
|
31
|
+
- name: Lint
|
|
32
|
+
if: ${{ !cancelled() }}
|
|
33
|
+
uses: astral-sh/ruff-action@278981a28ce3188b1e39527901f38254bf3aac89 # v4.1.0
|
|
34
|
+
with:
|
|
35
|
+
args: "check --output-format=github"
|
|
36
|
+
|
|
37
|
+
frontend:
|
|
38
|
+
name: Frontend and API contract (Python ${{ matrix.python-version }})
|
|
39
|
+
runs-on: ubuntu-latest
|
|
40
|
+
# Installing the package and exporting the OpenAPI schema is the only
|
|
41
|
+
# exercise the Python code gets, so run it on every version pyproject.toml
|
|
42
|
+
# claims to support. The UI-only steps below run once, on the newest.
|
|
43
|
+
strategy:
|
|
44
|
+
fail-fast: false
|
|
45
|
+
matrix:
|
|
46
|
+
python-version: ["3.11", "3.12", "3.13", "3.14"]
|
|
47
|
+
steps:
|
|
48
|
+
- uses: actions/checkout@v7
|
|
49
|
+
|
|
50
|
+
# Node must be set up before the Python install: the hatch-jupyter-builder
|
|
51
|
+
# hook in pyproject.toml shells out to npm, so `pip install .` aborts with
|
|
52
|
+
# "Could not find cmd (npm) in path" when Node is missing — even while
|
|
53
|
+
# only generating package metadata.
|
|
54
|
+
- uses: actions/setup-node@v7
|
|
55
|
+
with:
|
|
56
|
+
node-version: "24"
|
|
57
|
+
cache: npm
|
|
58
|
+
cache-dependency-path: course-service-ui/package-lock.json
|
|
59
|
+
|
|
60
|
+
- uses: actions/setup-python@v7
|
|
61
|
+
with:
|
|
62
|
+
python-version: ${{ matrix.python-version }}
|
|
63
|
+
|
|
64
|
+
- name: Install UI dependencies
|
|
65
|
+
run: npm ci
|
|
66
|
+
working-directory: course-service-ui
|
|
67
|
+
|
|
68
|
+
# Installing the project makes e2x_course_hub importable for the OpenAPI
|
|
69
|
+
# export below, and exercises the packaging path as a side effect: the
|
|
70
|
+
# build hook bundles the UI exactly as a released wheel would.
|
|
71
|
+
- name: Install the course hub
|
|
72
|
+
run: pip install .
|
|
73
|
+
|
|
74
|
+
# Regenerates openapi.json and schema.d.ts and fails if either differs
|
|
75
|
+
# from what is committed, so a backend schema change cannot land without
|
|
76
|
+
# the matching client types.
|
|
77
|
+
- name: Check the generated API types are up to date
|
|
78
|
+
run: npm run check:api-types
|
|
79
|
+
working-directory: course-service-ui
|
|
80
|
+
env:
|
|
81
|
+
PYTHON: python
|
|
82
|
+
|
|
83
|
+
- name: Lint
|
|
84
|
+
if: matrix.python-version == '3.14'
|
|
85
|
+
run: npm run lint
|
|
86
|
+
working-directory: course-service-ui
|
|
87
|
+
|
|
88
|
+
- name: Check formatting
|
|
89
|
+
if: matrix.python-version == '3.14'
|
|
90
|
+
run: npx prettier --check src
|
|
91
|
+
working-directory: course-service-ui
|
|
92
|
+
|
|
93
|
+
# Runs `tsc -b` and then the production bundle.
|
|
94
|
+
- name: Typecheck and build
|
|
95
|
+
if: matrix.python-version == '3.14'
|
|
96
|
+
run: npm run build
|
|
97
|
+
working-directory: course-service-ui
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
name: Upload Python Package
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
|
|
7
|
+
permissions:
|
|
8
|
+
contents: read
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
release-build:
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v7
|
|
16
|
+
|
|
17
|
+
- uses: actions/setup-python@v7
|
|
18
|
+
with:
|
|
19
|
+
python-version: "3.x"
|
|
20
|
+
|
|
21
|
+
- name: Build release distributions
|
|
22
|
+
run: |
|
|
23
|
+
# NOTE: put your own distribution build steps here.
|
|
24
|
+
python -m pip install build
|
|
25
|
+
python -m build
|
|
26
|
+
|
|
27
|
+
- name: Upload distributions
|
|
28
|
+
uses: actions/upload-artifact@v7
|
|
29
|
+
with:
|
|
30
|
+
name: release-dists
|
|
31
|
+
path: dist/
|
|
32
|
+
|
|
33
|
+
pypi-publish:
|
|
34
|
+
runs-on: ubuntu-latest
|
|
35
|
+
needs:
|
|
36
|
+
- release-build
|
|
37
|
+
permissions:
|
|
38
|
+
id-token: write
|
|
39
|
+
environment:
|
|
40
|
+
name: pypi
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
steps:
|
|
44
|
+
- name: Retrieve release distributions
|
|
45
|
+
uses: actions/download-artifact@v8
|
|
46
|
+
with:
|
|
47
|
+
name: release-dists
|
|
48
|
+
path: dist/
|
|
49
|
+
|
|
50
|
+
- name: Publish release distributions to PyPI
|
|
51
|
+
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
|
|
52
|
+
with:
|
|
53
|
+
packages-dir: dist/
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# Node modules
|
|
2
|
+
|
|
3
|
+
node_modules
|
|
4
|
+
|
|
5
|
+
# Meta files
|
|
6
|
+
ui/meta.json
|
|
7
|
+
|
|
8
|
+
# Build static files
|
|
9
|
+
share/e2x_course_hub/static/
|
|
10
|
+
|
|
11
|
+
# Byte-compiled / optimized / DLL files
|
|
12
|
+
__pycache__/
|
|
13
|
+
*.py[cod]
|
|
14
|
+
*$py.class
|
|
15
|
+
|
|
16
|
+
# C extensions
|
|
17
|
+
*.so
|
|
18
|
+
|
|
19
|
+
# Distribution / packaging
|
|
20
|
+
.Python
|
|
21
|
+
build/
|
|
22
|
+
develop-eggs/
|
|
23
|
+
dist/
|
|
24
|
+
downloads/
|
|
25
|
+
eggs/
|
|
26
|
+
.eggs/
|
|
27
|
+
lib/*
|
|
28
|
+
!src/lib/
|
|
29
|
+
lib64/
|
|
30
|
+
parts/
|
|
31
|
+
sdist/
|
|
32
|
+
var/
|
|
33
|
+
wheels/
|
|
34
|
+
pip-wheel-metadata/
|
|
35
|
+
share/python-wheels/
|
|
36
|
+
*.egg-info/
|
|
37
|
+
.installed.cfg
|
|
38
|
+
*.egg
|
|
39
|
+
MANIFEST
|
|
40
|
+
|
|
41
|
+
# PyInstaller
|
|
42
|
+
# Usually these files are written by a python script from a template
|
|
43
|
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
44
|
+
*.manifest
|
|
45
|
+
*.spec
|
|
46
|
+
|
|
47
|
+
# Installer logs
|
|
48
|
+
pip-log.txt
|
|
49
|
+
pip-delete-this-directory.txt
|
|
50
|
+
|
|
51
|
+
# Unit test / coverage reports
|
|
52
|
+
htmlcov/
|
|
53
|
+
.tox/
|
|
54
|
+
.nox/
|
|
55
|
+
.coverage
|
|
56
|
+
.coverage.*
|
|
57
|
+
.cache
|
|
58
|
+
nosetests.xml
|
|
59
|
+
coverage.xml
|
|
60
|
+
*.cover
|
|
61
|
+
*.py,cover
|
|
62
|
+
.hypothesis/
|
|
63
|
+
.pytest_cache/
|
|
64
|
+
|
|
65
|
+
# Translations
|
|
66
|
+
*.mo
|
|
67
|
+
*.pot
|
|
68
|
+
|
|
69
|
+
# Django stuff:
|
|
70
|
+
*.log
|
|
71
|
+
local_settings.py
|
|
72
|
+
db.sqlite3
|
|
73
|
+
db.sqlite3-journal
|
|
74
|
+
|
|
75
|
+
# Flask stuff:
|
|
76
|
+
instance/
|
|
77
|
+
.webassets-cache
|
|
78
|
+
|
|
79
|
+
# Scrapy stuff:
|
|
80
|
+
.scrapy
|
|
81
|
+
|
|
82
|
+
# Sphinx documentation
|
|
83
|
+
docs/_build/
|
|
84
|
+
|
|
85
|
+
# PyBuilder
|
|
86
|
+
target/
|
|
87
|
+
|
|
88
|
+
# Jupyter Notebook
|
|
89
|
+
.ipynb_checkpoints
|
|
90
|
+
|
|
91
|
+
# IPython
|
|
92
|
+
profile_default/
|
|
93
|
+
ipython_config.py
|
|
94
|
+
|
|
95
|
+
# pyenv
|
|
96
|
+
.python-version
|
|
97
|
+
|
|
98
|
+
# pipenv
|
|
99
|
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
|
100
|
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
|
101
|
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
|
102
|
+
# install all needed dependencies.
|
|
103
|
+
#Pipfile.lock
|
|
104
|
+
|
|
105
|
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
|
|
106
|
+
__pypackages__/
|
|
107
|
+
|
|
108
|
+
# Celery stuff
|
|
109
|
+
celerybeat-schedule
|
|
110
|
+
celerybeat.pid
|
|
111
|
+
|
|
112
|
+
# SageMath parsed files
|
|
113
|
+
*.sage.py
|
|
114
|
+
|
|
115
|
+
# Environments
|
|
116
|
+
.env
|
|
117
|
+
.venv
|
|
118
|
+
env/
|
|
119
|
+
venv/
|
|
120
|
+
ENV/
|
|
121
|
+
env.bak/
|
|
122
|
+
venv.bak/
|
|
123
|
+
|
|
124
|
+
# Spyder project settings
|
|
125
|
+
.spyderproject
|
|
126
|
+
.spyproject
|
|
127
|
+
|
|
128
|
+
# Rope project settings
|
|
129
|
+
.ropeproject
|
|
130
|
+
|
|
131
|
+
# mkdocs documentation
|
|
132
|
+
/site
|
|
133
|
+
|
|
134
|
+
# mypy
|
|
135
|
+
.mypy_cache/
|
|
136
|
+
.dmypy.json
|
|
137
|
+
dmypy.json
|
|
138
|
+
|
|
139
|
+
# Pyre type checker
|
|
140
|
+
.pyre/
|
|
141
|
+
|
|
142
|
+
# vscode
|
|
143
|
+
.vscode/
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2020 DigiKlausur
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: e2x-course-hub
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: A JupyterHub service for managing courses, terms, and course membership with role-based access control in multi-course teaching deployments.
|
|
5
|
+
Project-URL: Documentation, https://github.com/Digiklausur/e2x-course-hub#readme
|
|
6
|
+
Project-URL: Issues, https://github.com/Digiklausur/e2x-course-hub/issues
|
|
7
|
+
Project-URL: Source, https://github.com/Digiklausur/e2x-course-hub
|
|
8
|
+
Author-email: Tim Metzler <tim.metzler@h-brs.de>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Programming Language :: Python
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
17
|
+
Requires-Python: >=3.11
|
|
18
|
+
Requires-Dist: e2x-hub-rbac>=0.3.0
|
|
19
|
+
Requires-Dist: fastapi>=0.100.0
|
|
20
|
+
Requires-Dist: httpx>=0.24
|
|
21
|
+
Requires-Dist: jinja2>=3.1
|
|
22
|
+
Requires-Dist: jupyterhub-fastapi-adapter>=0.1.0
|
|
23
|
+
Requires-Dist: pydantic-settings>=2.0
|
|
24
|
+
Requires-Dist: pydantic>=2.0
|
|
25
|
+
Requires-Dist: python-multipart>=0.0.6
|
|
26
|
+
Requires-Dist: pyyaml>=6.0
|
|
27
|
+
Requires-Dist: sqlalchemy>=2.0
|
|
28
|
+
Requires-Dist: uvicorn>=0.23
|
|
29
|
+
Provides-Extra: dev
|
|
30
|
+
Requires-Dist: build; extra == 'dev'
|
|
31
|
+
Requires-Dist: pre-commit; extra == 'dev'
|
|
32
|
+
Requires-Dist: ruff==0.16.7; extra == 'dev'
|
|
33
|
+
Requires-Dist: tbump; extra == 'dev'
|
|
34
|
+
Requires-Dist: twine; extra == 'dev'
|
|
35
|
+
Provides-Extra: docs
|
|
36
|
+
Requires-Dist: autodoc-pydantic>=2.0; extra == 'docs'
|
|
37
|
+
Requires-Dist: myst-parser; extra == 'docs'
|
|
38
|
+
Requires-Dist: pydata-sphinx-theme; extra == 'docs'
|
|
39
|
+
Requires-Dist: sphinx; extra == 'docs'
|
|
40
|
+
Description-Content-Type: text/markdown
|
|
41
|
+
|
|
42
|
+
# e2x Course Hub
|
|
43
|
+
|
|
44
|
+
A JupyterHub service for running multi-course, multi-term teaching deployments. It manages courses, terms, and course membership; enforces who may do what
|
|
45
|
+
via role-based access control; and tells a separate infrastructure spawner which
|
|
46
|
+
courses/terms a user may launch and with which environment — without ever deciding
|
|
47
|
+
hardware itself.
|
|
48
|
+
|
|
49
|
+
It ships as a FastAPI backend (REST API under `/v1/...`) plus a React single-page app
|
|
50
|
+
(`course-service-ui`) for course and membership management, both served from the same
|
|
51
|
+
process.
|
|
52
|
+
|
|
53
|
+
## What it does
|
|
54
|
+
|
|
55
|
+
- **Course & term management** — courses and their terms are stored in a database
|
|
56
|
+
(SQLAlchemy) and managed through a REST API: create/remove courses and terms, edit
|
|
57
|
+
metadata, and configure the environment (image, resource tier, profile) per course,
|
|
58
|
+
overridable per term.
|
|
59
|
+
- **Role-based access control** — powered by [`e2x-hub-rbac`](https://github.com/Digiklausur/e2x-hub-rbac).
|
|
60
|
+
Roles (LMS admin, course creator, course owner, instructor, teaching assistant,
|
|
61
|
+
student, observer) are scoped to the LMS, a course, or a term, and membership is
|
|
62
|
+
backed by JupyterHub groups.
|
|
63
|
+
- **A typed contract with the spawner** — `e2x_course_hub.contract` defines the
|
|
64
|
+
boundary between this package and the JupyterHub Spawner that actually launches
|
|
65
|
+
containers. The course hub owns authorization and course configuration; the spawner
|
|
66
|
+
owns the runtime. They meet only through two `Protocol`s:
|
|
67
|
+
- `SpawnOfferingProvider` (implemented here): tells the spawner, for a given user,
|
|
68
|
+
which course/terms they may launch, in which role, with which selection.
|
|
69
|
+
- `InfrastructureCatalogProvider` (implemented by the spawner): tells the course hub
|
|
70
|
+
what images, resource tiers, and profiles actually exist, so course configuration
|
|
71
|
+
can only ever offer valid choices.
|
|
72
|
+
- **A management UI** — `course-service-ui`, a React SPA served by the same FastAPI app,
|
|
73
|
+
for browsing courses and managing membership in the browser.
|
|
74
|
+
|
|
75
|
+
See [`e2x_course_hub/contract/README.md`](e2x_course_hub/contract/README.md) for the
|
|
76
|
+
full contract reference, including the catalog/selection data model and how a spawn
|
|
77
|
+
plays out end to end.
|
|
78
|
+
|
|
79
|
+
## Architecture
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
e2x_course_hub/
|
|
83
|
+
├── contract/ # Protocol boundary shared with the infrastructure spawner
|
|
84
|
+
│ └── providers.py # InfrastructureCatalogProvider, SpawnOfferingProvider
|
|
85
|
+
│ # (re-exports the catalog/selection models from schema/ below)
|
|
86
|
+
├── schema/ # Pydantic models backing the contract and the API
|
|
87
|
+
│ ├── catalog.py # InfrastructureCatalogOptions and friends
|
|
88
|
+
│ ├── selection.py # SpawnSelection, SpawnOffering, CourseReference
|
|
89
|
+
│ ├── course.py # CourseConfig / TermConfig
|
|
90
|
+
│ └── types.py # SpawnRole, UserLike
|
|
91
|
+
├── db/ # SQLAlchemy models + CourseRepository
|
|
92
|
+
├── api/ # Business logic, framework-agnostic
|
|
93
|
+
│ ├── api.py # API — aggregates the sub-APIs below
|
|
94
|
+
│ ├── course_api.py # Course/term CRUD and environment configuration
|
|
95
|
+
│ ├── membership_api.py # Course membership (extends e2x-hub-rbac's MembershipAPI)
|
|
96
|
+
│ ├── infrastructure_api.py # Read-only access to the infrastructure catalog
|
|
97
|
+
│ ├── spawn_offering_api.py # Implements SpawnOfferingProvider for the spawner
|
|
98
|
+
│ └── *_permissions.py # Role → permission mappings per sub-API
|
|
99
|
+
├── course_service/ # The FastAPI service
|
|
100
|
+
│ ├── fastapi_app.py # App wiring: routers, auth, static files, SPA route
|
|
101
|
+
│ ├── settings.py # ServiceSettings (extends settings.CoreSettings)
|
|
102
|
+
│ ├── whoami.py # Minimal standalone service: returns the caller's identity
|
|
103
|
+
│ └── {courses,terms,infrastructure,membership,me}/ # One router + schemas per resource
|
|
104
|
+
├── loader.py # Composition root: settings → concrete API objects
|
|
105
|
+
└── settings.py # CoreSettings: hub API, DB, membership, infrastructure provider
|
|
106
|
+
|
|
107
|
+
course-service-ui/ # React 19 + TypeScript SPA, built with Vite
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### The two sides
|
|
111
|
+
|
|
112
|
+
| | Course hub (this repo) | Infrastructure spawner |
|
|
113
|
+
|------------|-----------------------------------------------------------------------|----------------------------------------------------|
|
|
114
|
+
| Knows | courses, terms, users, roles (via `e2x-hub-rbac`), permissions | what can actually be run: image families/tags, resource tiers, profiles |
|
|
115
|
+
| Implements | `SpawnOfferingProvider` | `InfrastructureCatalogProvider` |
|
|
116
|
+
| Consumes | `InfrastructureCatalogProvider` | `SpawnOfferingProvider` |
|
|
117
|
+
| Never does | translates a selection to hardware | decides who is allowed to do what |
|
|
118
|
+
|
|
119
|
+
The spawner is a separate, pluggable package. The course hub discovers it at startup through
|
|
120
|
+
the `e2x_course_hub.infrastructure_catalog_providers` entry-point group, selected by
|
|
121
|
+
name via the `E2X_INFRASTRUCTURE_PROVIDER` setting — the hub never imports it directly.
|
|
122
|
+
See [`e2x-course-hub-kubespawner`](https://github.com/DigiKlausur/e2x-course-hub-kubespawner)
|
|
123
|
+
for an example implementation based on KubeSpawner.
|
|
124
|
+
|
|
125
|
+
## Installation
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
pip install git+https://github.com/Digiklausur/e2x-course-hub.git
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
For development:
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
git clone https://github.com/Digiklausur/e2x-course-hub
|
|
135
|
+
cd e2x-course-hub
|
|
136
|
+
pip install -e ".[dev]"
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Building the package (`pip install .` or `python -m build`) also builds
|
|
140
|
+
`course-service-ui` via `hatch-jupyter-builder` and bundles the result into the wheel.
|
|
141
|
+
For frontend-only iteration:
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
cd course-service-ui
|
|
145
|
+
npm install
|
|
146
|
+
npm run dev
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Running it
|
|
150
|
+
|
|
151
|
+
The service is a standard [JupyterHub managed service](https://jupyterhub.readthedocs.io/en/stable/reference/services.html),
|
|
152
|
+
run with `uvicorn`:
|
|
153
|
+
|
|
154
|
+
```python
|
|
155
|
+
# jupyterhub_config.py
|
|
156
|
+
c.JupyterHub.services = [
|
|
157
|
+
{
|
|
158
|
+
"name": "course-service",
|
|
159
|
+
"url": "http://127.0.0.1:10105",
|
|
160
|
+
"command": ["uvicorn", "e2x_course_hub.course_service.fastapi_app:app"],
|
|
161
|
+
"environment": {
|
|
162
|
+
"UVICORN_HOST": "0.0.0.0",
|
|
163
|
+
"UVICORN_PORT": "10105",
|
|
164
|
+
"E2X_INFRASTRUCTURE_PROVIDER": "k8s_catalog",
|
|
165
|
+
"E2X_ADD_USERS_TO_HUB": "true",
|
|
166
|
+
"E2X_DELETE_EMPTY_GROUPS": "true",
|
|
167
|
+
"E2X_COURSE_DB_URL": "sqlite:////srv/jupyterhub/courses.db",
|
|
168
|
+
},
|
|
169
|
+
}
|
|
170
|
+
]
|
|
171
|
+
|
|
172
|
+
c.JupyterHub.load_roles = [
|
|
173
|
+
{
|
|
174
|
+
"name": "course-service",
|
|
175
|
+
"services": ["course-service"],
|
|
176
|
+
"scopes": [
|
|
177
|
+
"access:services!service=course-service",
|
|
178
|
+
"admin:users",
|
|
179
|
+
"list:users",
|
|
180
|
+
"delete:users",
|
|
181
|
+
"admin:groups",
|
|
182
|
+
],
|
|
183
|
+
}
|
|
184
|
+
]
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
`JUPYTERHUB_API_URL`, `JUPYTERHUB_API_TOKEN`, and `JUPYTERHUB_SERVICE_PREFIX` are
|
|
188
|
+
injected automatically by JupyterHub for a managed service and don't need to be set
|
|
189
|
+
explicitly.
|
|
190
|
+
|
|
191
|
+
### Configuration
|
|
192
|
+
|
|
193
|
+
All settings are read from the environment (via `pydantic-settings`):
|
|
194
|
+
|
|
195
|
+
| Variable | Default | Purpose |
|
|
196
|
+
|-------------------------------|-----------------------------|-----------------------------------------------------------------|
|
|
197
|
+
| `JUPYTERHUB_API_URL` | — | JupyterHub Hub API URL, used to manage users and groups |
|
|
198
|
+
| `JUPYTERHUB_API_TOKEN` | — | API token for the above |
|
|
199
|
+
| `JUPYTERHUB_SERVICE_PREFIX` | `/` | URL prefix the service is mounted under |
|
|
200
|
+
| `E2X_COURSE_DB_URL` | `sqlite:///courses.db` | SQLAlchemy URL for the course/term database |
|
|
201
|
+
| `E2X_INFRASTRUCTURE_PROVIDER` | `default` | Entry-point name of the `InfrastructureCatalogProvider` to load |
|
|
202
|
+
| `E2X_ADD_USERS_TO_HUB` | `false` | Create JupyterHub users automatically on membership changes |
|
|
203
|
+
| `E2X_DELETE_EMPTY_GROUPS` | `false` | Delete a course/term's JupyterHub group once it has no members |
|
|
204
|
+
|
|
205
|
+
### Optional: `whoami` service
|
|
206
|
+
|
|
207
|
+
`e2x_course_hub.course_service.whoami:app` is a minimal standalone FastAPI service that
|
|
208
|
+
returns the authenticated caller's identity — handy for verifying JupyterHub OAuth is
|
|
209
|
+
wired up correctly before debugging the full course service.
|
|
210
|
+
|
|
211
|
+
## REST API
|
|
212
|
+
|
|
213
|
+
All routes are served under `{service_prefix}/v1`. Interactive docs (Swagger UI) are
|
|
214
|
+
available at `{service_prefix}/docs` once the service is running, and the schema can be
|
|
215
|
+
exported standalone with `python scripts/export_openapi.py`.
|
|
216
|
+
|
|
217
|
+
| Resource | Routes |
|
|
218
|
+
|----------------------|------------------------------------------------------------------------------------------------------------|
|
|
219
|
+
| Courses | `GET/POST /courses`, `GET/DELETE /courses/{course_id}`, `GET/PATCH /courses/{course_id}/metadata`, `GET/PATCH /courses/{course_id}/environment`, `GET /courses/{course_id}/terms` |
|
|
220
|
+
| Terms | `POST/GET/DELETE /courses/{course_id}/terms/{term_id}`, `GET/PATCH /courses/{course_id}/terms/{term_id}/environment` |
|
|
221
|
+
| Infrastructure | `GET /catalogs/image-catalog`, `GET /catalogs/resource-tiers`, `GET /catalogs/profile-catalog` |
|
|
222
|
+
| Membership | `GET/PATCH /lms/admins`, `GET/PATCH /lms/course-creators`, `GET/PATCH /courses/{course_id}/owners`, `GET/PATCH /courses/{course_id}/terms/{term_id}/{instructors,teaching-assistants,students,observers}` |
|
|
223
|
+
| Roles | `GET /roles/{role}/actions` |
|
|
224
|
+
| Current user | `GET /me` |
|
|
225
|
+
|
|
226
|
+
`course-service-ui` consumes this API with types generated from the OpenAPI schema
|
|
227
|
+
(`npm run generate:api-types` in `course-service-ui/`); CI fails if the committed types
|
|
228
|
+
drift from the schema.
|
|
229
|
+
|
|
230
|
+
## Roles & permissions
|
|
231
|
+
|
|
232
|
+
Roles are defined by `e2x-hub-rbac` and scoped to the LMS, a course, or a term. Default
|
|
233
|
+
permissions for course/term management (`e2x_course_hub/api/course_permissions.py`):
|
|
234
|
+
|
|
235
|
+
| Role | Scope | Default permissions |
|
|
236
|
+
|---------------------|--------|----------------------------------------------------------------------------------------|
|
|
237
|
+
| LMS admin | LMS | Everything |
|
|
238
|
+
| Course creator | LMS | Create courses, view courses |
|
|
239
|
+
| Course owner | Course | Manage the course (metadata, environment, remove) and its terms (add/remove, environment) |
|
|
240
|
+
| Instructor | Term | View the course, manage the term (environment, remove) |
|
|
241
|
+
| Teaching assistant | Term | View the course and term |
|
|
242
|
+
| Observer | Term | View the course and term |
|
|
243
|
+
| Student | Term | (none — spawns only, no management permissions) |
|
|
244
|
+
|
|
245
|
+
Membership itself (who holds which role, per course/term) is managed separately through
|
|
246
|
+
the membership API, backed by JupyterHub groups. The membership, infrastructure catalog
|
|
247
|
+
and spawn permission tables, and the group names behind each role, are in
|
|
248
|
+
[`docs/source/reference/roles.md`](docs/source/reference/roles.md).
|
|
249
|
+
|
|
250
|
+
## Development
|
|
251
|
+
|
|
252
|
+
```bash
|
|
253
|
+
ruff check .
|
|
254
|
+
ruff format --check .
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
```bash
|
|
258
|
+
cd course-service-ui
|
|
259
|
+
npm run lint
|
|
260
|
+
npm run format
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
CI (`.github/workflows/ci.yml`) runs Python linting, frontend linting/formatting, and
|
|
264
|
+
checks that the generated OpenAPI schema and TypeScript types are committed and in
|
|
265
|
+
sync with the routers.
|
|
266
|
+
|
|
267
|
+
## License
|
|
268
|
+
|
|
269
|
+
MIT License — see [LICENSE](LICENSE) for details.
|