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
16 changes: 8 additions & 8 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,156 +2,156 @@
on: [push]
jobs:
python-tests:
runs-on: ubuntu-22.04
runs-on: ubuntu-24.04

services:
db:
image: postgres:15.15
# Health checks to wait until postgres has started
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres # pragma: allowlist secret
POSTGRES_DB: postgres
ports:
- 5432:5432

redis:
image: redis:8.4.0
ports:
- 6379:6379

steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6

- name: Apt update
run: sudo apt-get update -y

- name: Apt install
run: cat Aptfile | sudo xargs apt-get install

- name: Set up Python
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6
with:
python-version: "3.11"

- name: Install poetry
uses: snok/install-poetry@v1
with:
version: 2.1.3
virtualenvs-create: true
virtualenvs-in-project: true

- name: Set up Python
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6
with:
python-version: "3.10"

- name: Install dependencies
run: poetry install --no-interaction

- name: Create test local state
run: ./scripts/test/stub-data.sh

- name: Running Celery
run: |
celery -A main worker -B -l INFO &
sleep 10
env:
CELERY_TASK_ALWAYS_EAGER: 'True'
CELERY_BROKER_URL: redis://localhost:6379/4
CELERY_RESULT_BACKEND: redis://localhost:6379/4
SECRET_KEY: local_unsafe_key # pragma: allowlist secret
MITX_ONLINE_BASE_URL: http://localhost:8013
MAILGUN_SENDER_DOMAIN: other.fake.site
MAILGUN_KEY: fake_mailgun_key
MITX_ONLINE_ADMIN_EMAIL: example@localhost
OPENEDX_API_CLIENT_ID: fake_client_id
OPENEDX_API_CLIENT_SECRET: fake_client_secret # pragma: allowlist secret

- name: Django system checks
run: poetry run ./manage.py check --fail-level WARNING
env:
CELERY_TASK_ALWAYS_EAGER: 'True'
CELERY_BROKER_URL: redis://localhost:6379/4
CELERY_RESULT_BACKEND: redis://localhost:6379/4
SECRET_KEY: local_unsafe_key # pragma: allowlist secret
MITX_ONLINE_BASE_URL: http://localhost:8013
MAILGUN_SENDER_DOMAIN: other.fake.site
MAILGUN_KEY: fake_mailgun_key
MITX_ONLINE_ADMIN_EMAIL: example@localhost
OPENEDX_API_CLIENT_ID: fake_client_id
OPENEDX_API_CLIENT_SECRET: fake_client_secret # pragma: allowlist secret

- name: Tests
run: |
export MEDIA_ROOT="$(mktemp -d)"
cp scripts/test/data/webpack-stats/* webpack-stats/
./scripts/test/python_tests.sh
env:
DEBUG: False
NODE_ENV: 'production'
CELERY_TASK_ALWAYS_EAGER: 'True'
CELERY_BROKER_URL: redis://localhost:6379/4
CELERY_RESULT_BACKEND: redis://localhost:6379/4
DATABASE_URL: postgres://postgres:postgres@localhost:5432/postgres # pragma: allowlist secret
MAILGUN_KEY: fake_mailgun_key
MAILGUN_SENDER_DOMAIN: other.fake.site
MITX_ONLINE_ADMIN_EMAIL: example@localhost
MITX_ONLINE_BASE_URL: http://localhost:8013
MITX_ONLINE_DB_DISABLE_SSL: 'True'
MITX_ONLINE_EMAIL_BACKEND: django.core.mail.backends.locmem.EmailBackend
MITX_ONLINE_NOTIFICATION_EMAIL_BACKEND: django.core.mail.backends.locmem.EmailBackend
MITX_ONLINE_SECURE_SSL_REDIRECT: 'False'
MITX_ONLINE_USE_S3: 'False'
OPENEDX_API_BASE_URL: http://localhost:18000
OPENEDX_API_CLIENT_ID: fake_client_id
OPENEDX_API_CLIENT_SECRET: fake_client_secret # pragma: allowlist secret
SECRET_KEY: local_unsafe_key # pragma: allowlist secret

javascript-tests:

Check warning

Code scanning / CodeQL

Workflow does not contain permissions Medium

Actions job or workflow does not limit the permissions of the GITHUB_TOKEN. Consider setting an explicit permissions block, using the following as a minimal starting point: {contents: read}
runs-on: ubuntu-22.04
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6

- name: Setup NodeJS
uses: actions/setup-node@v2-beta
with:
node-version: "20.18"

- name: Get yarn cache directory path
id: yarn-cache-dir-path
run: echo "dir=$(yarn config get cacheFolder)" >> $GITHUB_OUTPUT

- uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5
- uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5
id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-

- name: Install dependencies
run: yarn install --immutable

- name: Lints
run: yarn workspaces foreach run lint

- name: Code formatting
run: yarn workspaces foreach run fmt:check

- name: Scss lint
run: yarn workspaces foreach run scss_lint

- name: Flow
run: yarn workspaces foreach run flow

- name: Tests
run: yarn workspaces foreach run test
env:
NODE_ENV: development

- name: Webpack production build
run: |
yarn workspaces foreach run build --bail --mode production
env:
NODE_ENV: production

Check warning

Code scanning / CodeQL

Workflow does not contain permissions Medium

Actions job or workflow does not limit the permissions of the GITHUB_TOKEN. Consider setting an explicit permissions block, using the following as a minimal starting point: {contents: read}
6 changes: 3 additions & 3 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,17 @@

jobs:
publish:
runs-on: ubuntu-22.04
runs-on: ubuntu-24.04
name: Publish Documentation
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
- name: Set up Python
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
with:
python-version: "3.10"
python-version: "3.11"
- name: Install ghp-import
run: pip install ghp-import
- name: Build documentation
run: "./pants docs ::"
- name: Publish Documentation
run: ghp-import --push --force --no-history --no-jekyll dist/sphinx/

Check warning

Code scanning / CodeQL

Workflow does not contain permissions Medium

Actions job or workflow does not limit the permissions of the GITHUB_TOKEN. Consider setting an explicit permissions block, using the following as a minimal starting point: {contents: read}
2 changes: 1 addition & 1 deletion .github/workflows/openapi-diff.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ permissions:
pull-requests: write
jobs:
openapi-diff:
runs-on: ubuntu-22.04
runs-on: ubuntu-24.04
steps:
- name: Checkout HEAD
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
Expand Down
4 changes: 2 additions & 2 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ repos:
hooks:
- id: shfmt
- repo: https://github.com/adrienverge/yamllint.git
rev: v1.37.1
rev: v1.38.0
hooks:
- id: yamllint
args: [--format, parsable, -d, relaxed]
Expand Down Expand Up @@ -47,7 +47,7 @@ repos:
- "config/keycloak/*"
additional_dependencies: ["gibberish-detector"]
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: "v0.14.11"
rev: "v0.14.13"
hooks:
- id: ruff-format
- id: ruff
Expand Down
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM python:3.10-slim AS base
FROM python:3.11-slim AS base

LABEL maintainer="ODL DevOps <mitx-devops@mit.edu>"

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

Version 0.137.8
---------------

- Call clear_expired to clear tokens (#3248)
- Update python 3.11 (#3105)
- Update dependency ubuntu to v24 (#3246)
- [pre-commit.ci] pre-commit autoupdate (#3228)
- Add ability to specify a certificate uuid directly to generate vc (#3240)
- Update actions/cache digest to 8b402f5 (#3242)
- Update actions/setup-python digest to a309ff8 (#3243)
- Update dependency pytest-cov to v7 (#3204)
- Improve v2/courses program field spec (#3238)
- Tweaks to `create_courseware_page` to populate a bit more data (#3237)
- Add order history/receipt APIs to OpenAPI spec. (#3234)
- add test for v2/courses api programs property (#3229)
- Add celery task to run cleartokens every week (#3232)
- Courses v2 org id tests (#3230)

Version 0.137.7 (Released January 21, 2026)
---------------

Expand Down
15 changes: 13 additions & 2 deletions cms/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,7 @@ def create_default_courseware_page(
live: bool = False,
include_in_learn_catalog: bool = False,
ingest_content_files_for_ai: bool = False,
optional_kwargs: dict | None = None,
):
"""
Creates a default about page for the given courseware object. Created pages
Expand Down Expand Up @@ -330,6 +331,9 @@ def create_default_courseware_page(
}
program_only_kwargs = {}

if optional_kwargs is None:
optional_kwargs = {}

try:
if isinstance(courseware, Course):
parent_page = CourseIndexPage.objects.filter(live=True).get()
Expand All @@ -339,9 +343,16 @@ def create_default_courseware_page(
raise ValidationError(f"No valid index page found for {courseware}.") # noqa: B904, EM102

if isinstance(courseware, Course):
page = CoursePage(course=courseware, **page_framework, **course_only_kwargs)
page = CoursePage(
course=courseware, **page_framework, **course_only_kwargs, **optional_kwargs
)
else:
page = ProgramPage(program=courseware, **page_framework, **program_only_kwargs)
page = ProgramPage(
program=courseware,
**page_framework,
**program_only_kwargs,
**optional_kwargs,
)

parent_page.add_child(instance=page)

Expand Down
98 changes: 88 additions & 10 deletions cms/management/commands/create_courseware_page.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@
Creates a basic courseware about page. This can be for programs or courses.
"""

import sys

from django.core.exceptions import ObjectDoesNotExist, ValidationError
from django.core.management import BaseCommand

from cms.api import create_default_courseware_page
from cms.models import Course, Program
from cms.models import Course, InstructorPage, InstructorPageLink, Program
from cms.utils import get_page_editing_url


Expand All @@ -17,6 +19,52 @@ class Command(BaseCommand):

help = "Creates a basic draft about page for the given courseware object."

def get_optional_values_for_courseware_type(
self, courseware_type: Course | Program
) -> dict:
"""
Returns a dictionary of optional values to include when creating the page,
based on the type of courseware (Course or Program).
"""

# Just some hardcoded example values for demonstration purposes.
# Might make sense to use faker for some of this or allow selection of values from different presets
# For now though, this sets up a page which is reasonably complete and can be immediately published
values = {
"price": [
(
"price_details",
{
"text": "PLACEHOLDER - Three easy payments of 99.99",
"link": "https://example.com/pricing",
},
)
],
"min_weeks": 1,
"max_weeks": 1,
"effort": "PLACEHOLDER - 1-2 hours per week",
"min_price": 37,
"max_price": 149,
"prerequisites": "PLACEHOLDER - No prerequisites, other than a willingness to learn",
"faq_url": "https://example.com",
}
if isinstance(courseware_type, Course):
values["about"] = (
"PLACEHOLDER - In this engineering course, we will explore the processing and structure of cellular solids as they are created from polymers, metals, ceramics, glasses and composites."
)
values["what_you_learn"] = (
"PLACEHOLDER - In this engineering course, we will explore the processing and structure of cellular solids as they are created from polymers, metals, ceramics, glasses and composites."
)
elif isinstance(courseware_type, Program):
values["about"] = (
"PLACEHOLDER - In this engineering program, we will explore the processing and structure of cellular solids as they are created from polymers, metals, ceramics, glasses and composites."
)
values["what_you_learn"] = (
"PLACEHOLDER - In this engineering program, we will explore the processing and structure of cellular solids as they are created from polymers, metals, ceramics, glasses and composites."
)

return values

def add_arguments(self, parser) -> None:
parser.add_argument(
"courseware_id",
Expand All @@ -37,8 +85,26 @@ def add_arguments(self, parser) -> None:
action="store_true",
help="Ingest content files for AI processing; courses-only. (Defaults to not.)",
)
parser.add_argument(
"--include_optional_values",
action="store_true",
help="Include more than bare minimum required fields while creating the page. By default these will not be populated",
)
parser.add_argument(
"--link_to_instructor",
action="store",
type=str,
default=None,
help="Comma separated list of instructor IDs to link to the courseware page.",
)

def error(self, message):
self.stdout.write(self.style.ERROR(message))
sys.exit(1)

def handle(self, *args, **kwargs): # pylint: disable=unused-argument # noqa: ARG002
include_optional_values = kwargs["include_optional_values"]
link_to_instructor = kwargs["link_to_instructor"]
try:
courseware = Course.objects.filter(
readable_id=kwargs["courseware_id"]
Expand All @@ -49,19 +115,22 @@ def handle(self, *args, **kwargs): # pylint: disable=unused-argument # noqa: A
readable_id=kwargs["courseware_id"]
).get()
except ObjectDoesNotExist:
self.stdout.write(
self.style.ERROR(
f"Can't find courseware object for {kwargs['courseware_id']}, stopping."
)
self.error(
f"Can't find courseware object for {kwargs['courseware_id']}, stopping."
)
return

try:
optional_kwargs = (
{}
if not include_optional_values
else self.get_optional_values_for_courseware_type(courseware)
)
page = create_default_courseware_page(
courseware,
live=kwargs["live"],
include_in_learn_catalog=kwargs["include_in_learn_catalog"],
ingest_content_files_for_ai=kwargs["ingest_content_files_for_ai"],
optional_kwargs=optional_kwargs,
)
self.stdout.write(
self.style.SUCCESS(
Expand All @@ -70,8 +139,17 @@ def handle(self, *args, **kwargs): # pylint: disable=unused-argument # noqa: A
)
)
except ValidationError as e:
self.stderr.write(
self.style.ERROR(
f"An error occurred creating the about page for {courseware.readable_id}: {e}"
)
self.error(
f"An error occurred creating the about page for {courseware.readable_id}: {e}"
)

if link_to_instructor:
instructor_ids = [
int(instructor_id)
for instructor_id in kwargs["link_to_instructor"].split(",")
]
instructor_pages = InstructorPage.objects.filter(id__in=instructor_ids)
for instructor_page in instructor_pages:
InstructorPageLink(
linked_instructor_page=instructor_page, page=page
).save()
6 changes: 2 additions & 4 deletions courses/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -1294,12 +1294,10 @@ def import_courserun_from_edx( # noqa: C901, PLR0913
course_page = create_default_courseware_page(
courseware=new_run.course,
live=publish_cms_page,
ingest_content_files_for_ai=ingest_content_files_for_ai,
include_in_learn_catalog=include_in_learn_catalog,
)

course_page.ingest_content_files_for_ai = ingest_content_files_for_ai
course_page.include_in_learn_catalog = include_in_learn_catalog
course_page.save()

course_product = None
if price:
content_type = ContentType.objects.get_for_model(CourseRun)
Expand Down
2 changes: 1 addition & 1 deletion courses/api_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -1212,7 +1212,7 @@ def test_override_user_grade(grade, letter_grade, should_force_pass, is_passed):
test_grade.refresh_from_db()
assert test_grade.grade == grade
assert test_grade.passed is is_passed
assert test_grade.letter_grade is letter_grade
assert test_grade.letter_grade == letter_grade
assert test_grade.set_by_admin is True


Expand Down
Loading