An LTI tool that allows for creating assignments using Jupyter Notebooks or Labs.
This is a hack week project completed in order to learn more about LTI 1.3. This is in no way production ready but may serve as a resource for learning how one might go about creating a proper LTI tool for Jupyter Notebooks. It has only been tested with Canvas, but should work with any LTI 1.3 compliant LMS.
The LTI compliant API is implemented using Flask.
Current endpoints are /login
, /launch
, /submit/<launch_id>
, as well as additional configuration content being served from /.well-known/
and static content from /static
.
Jupyter Notebook/Lab instances are hosted using Docker container built from src/nb and data is persisted using volumes named via resource_id
for Build launches and launch_id
for Taking launches.
Redis is used for caching Flask and LTI session data. Assignment and Submission data is persisted by using deterministic volume names in Docker. This was sufficient for this project but something more reliable and scalable shouls be implemented.
All Notebook launches are served using the image generated by src/nb/Dockerfile which launches a Jupyter Notebook or Labs server which will load Notebooks from the attached volume.
When a Build launch is initiated for the first time, a volume is created using the LTI resource_id
that will serve as the base for all student volumes.
The teacher can then configure the Jupyter Notebook with whatever files and configuration they wish for students to start work from.
When each student first initiates a Taking launch, a new volume is created using their launch_id
and the contents of the base notebook will be cloned into it.
When the teacher or student launches again, their volume will be reused. Note that subsequent changes to the base notebook by the teacher are possible but will not update notebooks for students who have already initiated a Taking launch as the base notebook is only cloned once per student.
All Docker orchestration is handled by the DockerAgent. Currently container names are static (via constant JUPYTER_CONTAINER_NAME
) and only a single Taking/Build container can be running at a time. This can easily be changed by updating the relevant code in DockerAgent.launch_container
to generate unique names for each launch. If this is done, some sort of cleanup should be implemented to prevent the build up of idle containers.
Container hostnames are generated dynamically and proxied using Dinghy HTTP Proxy which configures access for each container based on the VIRTUAL_HOST
and VIRTUAL_PORT
variables. This should probably be replaced with something more modern and reliable.
Once a student clicks the Submit button, a call is made to /submit/<launch_id>
which will post to Canvas-LMS grading API setting the activity progress to Completed and grading_progress to PendingReview. The student will then be redirected to a success or failure page. There is no other functionality implemented beyond this point. There is also no authentication of this call, anyone can submit an assignment if they know the launch_id
.
Flask configuration is stored in flask_config.json
LTI Tool configuration is stored in tool_config.json
A number of CONSTANTS can be configured in constants.py
Docker configuration is via ENV vars and is documented in docker_agent.py
src/app/config/ - Config files for Flask and LTI.
src/app/static/ - Static files.
src/app/templates/ - HTML template files (launch.html and submit.html)
src/app/well-known - This is where well-known configuration files are served from.
src/app/constants.py - Global configuration constants.
src/app/docker_agent.py - DockerAgent class which manages Docker container and volumes.
src/app/grading.py - Helpers for updating grading status.
src/app/helpers.py - Helpers for loading configuration, generating static well-known configuration files, etc.
src/app/main.py - The Flask application and routes.
src/app/setup.py - Packaging configuration.