This repository is the home of MIT Open Learning's reusable django apps.
This set of libraries is managed using uv.
To run this app in local development mode, copy testapp/main/settings/example.dev.py to testapp/main/settings/dev.py. This file has the same defaults as testapp/main/settings/test.py, but it is gitignored so you can safely add secrets to it. manage.py and main/wsgi.py both load dev.py.
- Install
xmlsecnative libraries for your OS: https://xmlsec.readthedocs.io/en/stable/install.html - Install
uvas described in the manual: https://docs.astral.sh/uv/ - Bootstrap the
uvenvironment:uv python install 3.11 ; uv sync
The Compose environment includes a container for general use called shell. You'll get a shell with uv already set up, and with a PostgreSQL database available.
- Ensure that 'other' users can write to the repo directory:
chmod -R o+w . - Build the containers:
docker compose build - Get a shell in the
shellcontainer:docker compose run --rm -ti shell bash
The database server is exposed on port 55432 locally - you can override this by setting POSTGRES_PORT in your environment.
- Django applications follow the naming convention
mitol-django-{name},pipinstallable by the same name. - Within each app, code is implemented under the implicit namespace
mitol- Module paths follow the pattern
mitol.{name} - The app itself is installable to
INSTALLED_APPSas"{name}".
- Module paths follow the pattern
Apps go in the src/ folder. Test suites for apps go in the tests/ folder (which is a Django app for this purpose).
Per convention, use _ for spaces within your app name if you must use spaces.
To add a new one, it's easiest to copy one of the existing apps. There's one called uvtestapp that has (basically) nothing in it, and can be used for this purpose.
- Duplicate the
uvtestappfolder, and rename the copy to the name you wish to use. - Update things within the folder to use the new name. This will include:
- The folder under
mitol README.mdpyproject.tomlmitol/<appname>/__init__.pymitol/<appname>/apps.py
- The folder under
- Update the root
pyproject.toml- Under
[project], add the new app intodependenciesin the same format that's already there. - Under
[tool.uv.sources], add a new entry for the new app, using (again) the same format as the other entries.
- Under
- Test building:
uv build --package mitol-django-<appname>. (This ensures that uv is OK with your changes.) - Add space for the app in the
testsapp:mkdir tests/mitol/<appname>and add a blank__init__.pyto it. - Add the app to
testapp/main/settings/shared.py- You must add it to
INSTALLED_APPS. - If your app has configuration settings, add to the
import_settings_modulecall at the top too.
- You must add it to
- Before the first release, register
mitol-django-<appname>as a pending publisher on PyPI. Without it the first publish fails. See PyPI Trusted Publishing.
You can now add your code and tests.
You can run Django commands by using the testapp that's included:
uv run tests/manage.py
The management commands for each ol-django app should be available. If you need to run things that require a database, run it in the Docker Compose setup as it contains a PostgreSQL database.
Run uv run pytest. This should run all the tests. If you want to run a specific one, specify with a file path as per usual. Use the whole path (so tests/mitol/<appname>/etc).
If you want to run the full test suite for the CI Python/Django matrix (Python 3.11-3.13 and Django 5.2), install tox and run:
uv tool install tox --with tox-uv
toxHooks live in .pre-commit-config.yaml and are run by prek, which uv sync installs. The config format is unchanged, so the file stays readable by pre-commit too.
- Install the git hook:
uv run prek install -f. The-freplaces an existingpre-commithook, if you have one installed from before. - Run every hook over the whole repo:
uv run prek run --all-files
CI runs the same hooks in the prek check (.github/workflows/autofix.yml), and autofix.ci pushes a commit with any fixes to your PR.
We maintain changelogs in changelog.d/ directories with each app. To create a new changelog for your changes, run:
uv run scripts/changelog.py create --app APPNAMEAPPNAME: the name of an application directory
You will need to adjust permissions/ownership on the new file if you're using the Compose setup.
Then fill out the new file that was generated with information about your changes. Do this before you put up a PR for your changes.
A fragment ships in the same PR as the code it describes, and that PR must not touch the app's CHANGELOG.md. Folding fragments into CHANGELOG.md is what cutting a release does, and it happens in its own PR — see below. uv run scripts/changelog.py check enforces the split, so a reviewer reading a code PR only ever has to read the new fragments.
Changelogs are maintained according to Keep a Changelog.
Versioning uses a date-based versioning scheme with incremental builds on the same day.
The exception is edx-api-client, which uses semantic versioning because its
consumers pin semver versions (for example >=1.14.0,<2).
Version tags follow {package-name}/v{version} and are created by CI.
A release is a version change merged to main. There are no tags to push and
no release commands to run against the remote.
Cutting a release is its own PR, containing nothing but the release. The fragments it collects were written earlier, each alongside the code change it describes.
-
Start from an up-to-date
mainwith no other work in progress. -
Prepare the release on a branch:
uv run scripts/release.py prepare --app APPNAME
For
edx_api_client, say which part to bump:--bump major,--bump minoror--bump patch.This bumps the app's version everywhere it is declared, folds its
changelog.d/fragments intoCHANGELOG.md, deletes them, and refreshesuv.lock. -
Commit the result, open a PR, and merge it once CI is green.
Once the checks pass on main, the publish job in
the CI workflow builds every package whose version
is not yet on PyPI, uploads it, and then creates the version tag.
Do not add code to a release PR. CI rejects a PR that rewrites an app's
CHANGELOG.md while changing anything under that app other than the two files
that declare its version.
If you would rather not use prepare, editing the version by hand works too —
the workflow only reads [project] version from the app's pyproject.toml. Keep
the other declarations ([tool.bumpver] current_version and the files in
[tool.bumpver.file_patterns], normally mitol/APPNAME/__init__.py) in step, or uv run scripts/version.py check fails
CI.
For each app it compares the version in pyproject.toml against PyPI, and
publishes only what is missing there. PyPI is the source of truth rather than
the tag history, because this repository contains published versions that were
never tagged. Practical consequences:
- Re-running the workflow is safe; already-published versions are skipped.
- Bumping several apps in one PR releases all of them.
- Editing a
pyproject.tomlwithout changing its version releases nothing. - The tag is created only after a successful upload, so a tag always means the version really is on PyPI.
Publishing uses Trusted Publishing: GitHub mints a short-lived OIDC token for the job, so there is no PyPI API token in this repository.
Each PyPI project must be told to trust this workflow once, under Manage project → Publishing:
| Field | Value |
|---|---|
| Owner | mitodl |
| Repository | ol-django |
| Workflow name | ci.yml |
| Environment | pypi |
The values are identical for every package. A package whose publisher is not yet configured simply fails its own matrix job, without affecting the others.
When adding a new package, register it as a pending publisher with the same values before its first release, since the PyPI project will not exist yet.
A missing publisher does not fail the OIDC token exchange, because other projects already trust this workflow. The upload fails instead:
| Upload error | Fix |
|---|---|
400 Non-user identities cannot create new projects |
The project is not on PyPI yet, and no pending publisher for it matches all four values. Add one, or replace a mismatched one, under Your account → Publishing. |
403 Invalid API Token: OIDC scoped token is not valid for project |
The project exists, but none of its publishers matches all four values. Add one under Manage project → Publishing. |
PyPI requires every field to match, so a publisher with the right workflow but a
different environment (such as mitodl instead of pypi) does not match. PyPI
cannot edit a publisher: add a corrected one, then remove the old one.
The publish job adds an error annotation naming the case that applies. Once the
publisher is registered, re-run the failed jobs (gh run rerun <run-id> --failed).