Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 7 additions & 11 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,20 +34,16 @@ jobs:
- name: Apt install
run: cat Aptfile | sudo xargs apt-get install

- name: Set up Python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
- name: Install uv
uses: astral-sh/setup-uv@v7
with:
python-version: "3.11"
enable-cache: true

- name: Install poetry
uses: snok/install-poetry@v1
with:
version: 2.1.3
virtualenvs-create: true
virtualenvs-in-project: true
- name: Install Python
run: uv python install 3.11

- name: Install dependencies
run: poetry install --no-interaction
run: uv sync --locked --no-group prod

- name: Create test local state
run: ./scripts/test/stub-data.sh
Expand All @@ -69,7 +65,7 @@ jobs:
OPENEDX_API_CLIENT_SECRET: fake_client_secret # pragma: allowlist secret

- name: Django system checks
run: poetry run ./manage.py check --fail-level WARNING
run: uv run ./manage.py check --fail-level WARNING
env:
CELERY_TASK_ALWAYS_EAGER: 'True'
CELERY_BROKER_URL: redis://localhost:6379/4
Expand Down
28 changes: 14 additions & 14 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,25 +21,25 @@ MITx Online is a Django-based web platform for managing MIT online courses and p
**Run tests:**
```bash
# Full test suite with parallel execution
poetry run pytest -n logical
uv run pytest -n logical

# Single test file
poetry run pytest courses/api_test.py
uv run pytest courses/api_test.py

# Single test function
poetry run pytest courses/api_test.py::test_function_name
uv run pytest courses/api_test.py::test_function_name

# With coverage
poetry run pytest --cov . --cov-report html
uv run pytest --cov . --cov-report html
```

**Linting and formatting:**
```bash
# Format code (ruff)
poetry run ruff format .
uv run ruff format .

# Lint with auto-fix
poetry run ruff check --fix .
uv run ruff check --fix .

# Pre-commit hooks (runs all checks)
pre-commit run --all-files
Expand Down Expand Up @@ -199,7 +199,7 @@ course = CourseFactory.create()

**Always check for missing migrations before committing:**
```bash
poetry run python manage.py makemigrations --check --dry-run
uv run python manage.py makemigrations --check --dry-run
```

Test suite includes checks for:
Expand Down Expand Up @@ -250,9 +250,9 @@ These provide standard patterns used across MIT ODL projects.

### Dependency Management

**Python:** Use Poetry for dependency management:
**Python:** Use uv for dependency management:
```bash
docker compose run --rm web poetry add <package>
docker compose run --rm web uv add <package>
docker compose build web celery # Rebuild images after changes
```

Expand Down Expand Up @@ -298,16 +298,16 @@ Optional OIDC authentication via Keycloak:
The test suite uses `pytest-django` with parallel execution via `pytest-xdist`:
```bash
# Single test module
poetry run pytest courses/models_test.py
uv run pytest courses/models_test.py

# Single test class
poetry run pytest courses/models_test.py::TestCourse
uv run pytest courses/models_test.py::TestCourse

# Single test method
poetry run pytest courses/models_test.py::TestCourse::test_course_creation
uv run pytest courses/models_test.py::TestCourse::test_course_creation

# With debugging (disables parallel execution)
poetry run pytest -n0 -s courses/models_test.py::test_function
uv run pytest -n0 -s courses/models_test.py::test_function
```

### Common Management Commands
Expand All @@ -331,7 +331,7 @@ manage.py collectstatic # Collect static files
Generate and view API schema:
```bash
# Generate schema
poetry run python manage.py spectacular --file schema.yml
uv run python manage.py spectacular --file schema.yml

# View in browser
# Navigate to http://mitxonline.odl.local:8013/api/schema/swagger-ui/
Expand Down
34 changes: 12 additions & 22 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -20,46 +20,36 @@ RUN mkdir /src && \
adduser --disabled-password --gecos "" mitodl && \
mkdir /var/media && chown -R mitodl:mitodl /var/media

FROM system AS poetry
FROM system AS uv

# copy in trusted certs
COPY --chmod=644 certs/*.crt /usr/local/share/ca-certificates/
RUN update-ca-certificates

# Poetry env configuration
# uv env configuration
ENV \
# poetry:
PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1 \
PIP_DISABLE_PIP_VERSION_CHECK=on \
POETRY_VERSION=2.1.3 \
POETRY_VIRTUALENVS_CREATE=false \
POETRY_CACHE_DIR='/tmp/cache/poetry' \
POETRY_HOME='/home/mitodl/.local' \
VIRTUAL_ENV="/opt/venv"
ENV PATH="$VIRTUAL_ENV/bin:$POETRY_HOME/bin:$PATH"
UV_PROJECT_ENVIRONMENT="/opt/venv"
ENV PATH="/opt/venv/bin:$PATH"

RUN pip install --no-cache-dir "poetry==$POETRY_VERSION"
# Install uv
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /usr/local/bin/

COPY pyproject.toml /src
COPY poetry.lock /src
COPY uv.lock /src
COPY mitol_*.gz /src

RUN chown -R mitodl:mitodl /src && \
mkdir ${VIRTUAL_ENV} && chown -R mitodl:mitodl ${VIRTUAL_ENV}
mkdir -p /opt/venv && \
chown -R mitodl:mitodl /opt/venv

USER mitodl
RUN curl -sSL https://install.python-poetry.org \
| \
POETRY_VERSION=${POETRY_VERSION} \
POETRY_HOME=${POETRY_HOME} \
python3 -q
WORKDIR /src
RUN python3 -m venv $VIRTUAL_ENV
RUN poetry install
RUN uv sync --frozen --no-install-project --group prod


FROM poetry AS code
FROM uv AS code

COPY . /src
WORKDIR /src
Expand Down Expand Up @@ -90,6 +80,6 @@ COPY --from=node /src /src

FROM code AS jupyter-notebook

RUN pip install --force-reinstall jupyter
RUN uv pip install --force-reinstall jupyter

USER mitodl
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,13 @@ In order to manage the certificates, follow these steps:

# Updating python dependencies

Python dependencies are managed with poetry. If you need to add a new dependency, run this command:
Python dependencies are managed with uv. If you need to add a new dependency, run this command:

```
docker compose run --rm web poetry add <dependency>
docker compose run --rm web uv add <dependency>
```
This will update the `pyproject.toml` and `poetry.lock` files. Then run `docker-compose build web celery` to make the change permanent in your docker images.
Refer to the [poetry documentation](https://python-poetry.org/docs/cli/) for particulars about specifying versions, removing dependencies, etc.
This will update the `pyproject.toml` and `uv.lock` files. Then run `docker-compose build web celery` to make the change permanent in your docker images.
Refer to the [uv documentation](https://docs.astral.sh/uv/reference/cli/) for particulars about specifying versions, removing dependencies, etc.


# Generating documentation
Expand Down
12 changes: 12 additions & 0 deletions RELEASE.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,18 @@
Release Notes
=============

Version 0.139.0
---------------

- Fix create_user and usages to be more robust (#3330)
- display program enrollment_modes in django admin (#3341)
- Add enrollment modes to the program APIs (#3339)
- Fix program enroll / unenroll APIs (#3336)
- Fix some issues with ensure_enrollment_codes_exist (#3329)
- Fix 500 error on program checkout completion (#3338)
- Move calls to create_user to earlier in the ecommerce process (#3337)
- chore: migrate from poetry/pip to uv for dependency management (#3327)

Version 0.138.7 (Released March 02, 2026)
---------------

Expand Down
50 changes: 27 additions & 23 deletions b2b/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@
)
from main import constants as main_constants
from main.utils import date_to_datetime
from openedx.api import create_user
from openedx.tasks import clone_courserun

log = logging.getLogger(__name__)
Expand Down Expand Up @@ -782,11 +781,14 @@ def _handle_unlimited_seats(


def _handle_limited_seats(
contract: ContractPage, product: Product, product_discounts: list[Discount]
contract: ContractPage, product: Product
) -> tuple[int, int, int]:
"""Handle limited seat contracts by creating/updating multiple discounts."""
created = updated = errors = 0
discount_amount = contract.enrollment_fixed_price or Decimal(0)
contract_product_discounts_qset = contract.get_discounts().filter(
products__product=product
)

if not contract.max_learners:
log.info("Contract %s doesn't have a learner cap, skipping", contract)
Expand All @@ -797,11 +799,13 @@ def _handle_limited_seats(
)

log.info(
"Updating %s discount codes for product %s", len(product_discounts), product
"Updating %s discount codes for product %s",
contract_product_discounts_qset.count(),
product,
)

# Update existing discounts
for discount in product_discounts:
for discount in contract_product_discounts_qset.all():
_update_discount(discount, discount_amount, REDEMPTION_TYPE_ONE_TIME)
discount.refresh_from_db()

Expand All @@ -822,15 +826,22 @@ def _handle_limited_seats(
updated += 1

# Create additional discounts if needed
create_count = contract.max_learners - contract.get_discounts().count()
log.info("Creating %s new discount codes for product %s", create_count, product)
current_discount_count = contract_product_discounts_qset.count()
create_count = contract.max_learners - current_discount_count
log.info(
"Contract %s has %s max learners and product %s has %s discounts",
contract,
contract.max_learners,
product,
current_discount_count,
)

if create_count < 0:
log.warning(
"ensure_enrollment_codes_exist: Seat limited contract %s product %s has too many discount codes: %s - removing extras",
contract,
product,
contract.get_products().count(),
current_discount_count,
)
errors = _handle_extra_enrollment_codes(contract, product)
log.warning(
Expand All @@ -841,6 +852,8 @@ def _handle_limited_seats(
)
return (created, updated, errors)

log.info("Creating %s discounts for product %s", create_count, product)

for _ in range(create_count):
discount = _create_discount_with_product(
product, discount_amount, REDEMPTION_TYPE_ONE_TIME
Expand Down Expand Up @@ -900,20 +913,16 @@ def ensure_enrollment_codes_exist(contract: ContractPage):
total_created = total_updated = total_errors = 0

for product in products:
product_discounts = list(
Discount.objects.filter(products__product=product).distinct()
)

if not contract.max_learners:
# Unlimited seats - one discount per product
created, updated, errors = _handle_unlimited_seats(
contract, product, product_discounts
contract,
product,
Discount.objects.filter(products__product=product).distinct(),
)
else:
# Limited seats - multiple discounts per product
created, updated, errors = _handle_limited_seats(
contract, product, product_discounts
)
created, updated, errors = _handle_limited_seats(contract, product)

total_created += created
total_updated += updated
Expand Down Expand Up @@ -957,9 +966,9 @@ def _prepare_basket_for_b2b_enrollment(request, product: Product) -> Basket:
"""
from ecommerce.api import establish_basket # noqa: PLC0415

basket = establish_basket(request)
# Clear the basket. For Unified Ecommerce, we may want to change this.
# But MITx Online only allows one item per cart and not clearing it is confusing.
# Establish and clear the basket. If the user doesn't have an edX username,
# don't wait to create one.
basket = establish_basket(request, no_delay=True)
basket.basket_items.all().delete()
basket.discounts.all().delete()

Expand Down Expand Up @@ -1024,11 +1033,6 @@ def create_b2b_enrollment(request, product: Product):
if validation_error:
return validation_error

# Check for an edX user, and create one if there's not one
if not request.user.edx_username:
create_user(request.user)
request.user.refresh_from_db()

# Prepare the basket for enrollment
basket = _prepare_basket_for_b2b_enrollment(request, product)

Expand Down
Loading
Loading