Skip to content
Merged
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
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ jobs:
- 6379:6379

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

- name: Apt update
run: sudo apt-get update -y
Expand Down Expand Up @@ -107,7 +107,7 @@ jobs:
javascript-tests:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6

- name: Setup NodeJS
uses: actions/setup-node@v2-beta
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ jobs:
runs-on: ubuntu-24.04
name: Publish Documentation
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Set up Python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
with:
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/openapi-diff.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@ jobs:
runs-on: ubuntu-24.04
steps:
- name: Checkout HEAD
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
ref: ${{ github.head_ref }}
path: head
- name: Checkout BASE
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
ref: ${{ github.base_ref }}
path: base
Expand Down
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ repos:
- "config/keycloak/*"
additional_dependencies: ["gibberish-detector"]
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: "v0.15.1"
rev: "v0.15.6"
hooks:
- id: ruff-format
- id: ruff
Expand Down
11 changes: 11 additions & 0 deletions RELEASE.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
Release Notes
=============

Version 1.142.3
---------------

- Update order receipt email template (#3402)
- Update dependency pyOpenSSL to v26 [SECURITY] (#3389)
- Fix incorrect API endpoint usage for course mode creation (#3394)
- Update dependency deepdiff to v8.6.2 [SECURITY] (#3399)
- [pre-commit.ci] pre-commit autoupdate (#3325)
- Update actions/checkout digest to de0fac2 (#3296)
- adding prefetch for products in programs v2 api (#3386)

Version 1.142.2 (Released March 19, 2026)
---------------

Expand Down
14 changes: 13 additions & 1 deletion courses/serializers/v2/programs.py
Original file line number Diff line number Diff line change
Expand Up @@ -556,11 +556,23 @@ class Meta:
class ProgramDetailSerializer(ProgramSerializer):
"""Extended Program serializer that includes products. Used by the programs API."""

products = ProductRelatedField(many=True, read_only=True)
products = serializers.SerializerMethodField()

class Meta(ProgramSerializer.Meta):
fields = [*ProgramSerializer.Meta.fields, "products"]

@extend_schema_field(ProductRelatedField(many=True, read_only=True))
def get_products(self, instance):
# Use prefetched products if available, otherwise fallback to related manager
products = getattr(instance, "prefetched_products", None)
if products is not None:
return ProductRelatedField(many=True, read_only=True).to_representation(
products
)
return ProductRelatedField(many=True, read_only=True).to_representation(
instance.products.all()
)


class ProgramCertificateSerializer(serializers.ModelSerializer):
"""ProgramCertificate model serializer"""
Expand Down
12 changes: 11 additions & 1 deletion courses/views/v2/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,16 @@ class ProgramViewSet(ReadableIdLookupMixin, viewsets.ReadOnlyModelViewSet):
lookup_value_regex = "[^/]+" # Accept any non-slash character

def get_queryset(self):
# Prefetch only products related to Program objects
program_content_type = ContentType.objects.get_for_model(Program)
program_product_queryset = Product.objects.filter(
content_type=program_content_type
)
products_prefetch = Prefetch(
"products",
queryset=program_product_queryset,
to_attr="prefetched_products",
)
return (
Program.objects.filter()
.select_related("page", "page__feature_image")
Expand Down Expand Up @@ -223,7 +233,7 @@ def get_queryset(self):
"collection_memberships__collection",
queryset=ProgramCollection.objects.only("id", "title"),
),
"products",
products_prefetch,
)
.order_by("title")
)
Expand Down
23 changes: 23 additions & 0 deletions courses/views/v2/views_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -2037,3 +2037,26 @@ def test_course_run_and_product_prefetch_optimized(
assert len(product_queries) == 2, (
f"Expected 1 product query, got {len(product_queries)}: {[q['sql'] for q in product_queries]}"
)


def test_program_products_prefetch_query_count(
user_drf_client, django_assert_max_num_queries
):
"""Test that products are prefetched and only one query is made for products in the program API."""
programs = ProgramFactory.create_batch(5, live=True)
for program in programs:
ProductFactory(purchasable_object=program)

num_queries_before = len(connection.queries)
expected_num_queries = num_queries_from_programs(programs, "v2")
with django_assert_max_num_queries(expected_num_queries) as context:
user_drf_client.get(reverse("v2:programs_api-list"))
duplicate_queries_check(context)
queries_after = connection.queries[num_queries_before:]

product_queries = [
q for q in queries_after if 'FROM "ecommerce_product"' in q.get("sql", "")
]
assert len(product_queries) == 1, (
f"Expected 1 product query, got {len(product_queries)}"
)
2 changes: 1 addition & 1 deletion ecommerce/templates/mail/product_order_receipt/body.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<td class="rec-head" style="color: rgba(0,0,0,0.85); font: 14px/20px 'Arial', sans-serif; max-width: 696px; ">
<p style="font-weight: normal; margin: 0 0 20px;">Dear {{ purchaser.name }},</p>
<p style="font-weight: normal; margin: 0 0 20px;">
You have been enrolled {% if content_title %} in {{ content_title }}{% endif %}
You have been enrolled{% if content_title %} in {{ content_title }}{% endif %}.
The {% if lines and lines.0.content_type %}
{% if lines.0.content_type == "program" %}program{% else %}course{% endif %}
{% endif %} should now appear on your MITxOnline <a href="{{ base_url }}{% url 'user-dashboard' %}" style="color: #0070DA">dashboard</a>. You can also access your receipt by <a style="color: #0070DA" href="{{ base_url }}/orders/receipt/{{order.id }}/">clicking here</a>.
Expand Down
2 changes: 1 addition & 1 deletion main/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
from main.sentry import init_sentry
from openapi.settings_spectacular import open_spectacular_settings

VERSION = "1.142.2"
VERSION = "1.142.3"

log = logging.getLogger()

Expand Down
12 changes: 3 additions & 9 deletions openedx/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -1543,7 +1543,7 @@ def update_edx_course( # noqa: PLR0913
)


def fix_cloned_run_data(target_course: CourseRun, edx_client) -> CourseRun:
def fix_cloned_run_data(target_course: CourseRun, *, edx_client=None) -> CourseRun:
"""Fix the title, pacing, and dates for a newly cloned run."""

course_params = [
Expand Down Expand Up @@ -1627,14 +1627,8 @@ def process_course_run_clone(target_course: CourseRun, base_course_key: str):
# We should have the target course in edX now. We need to update it with the
# data from our course run.
# Run these after the transaction that this will most likely be
transaction.on_commit(
partial(fix_cloned_run_data, target_course=target_course, edx_client=edx_client)
)
transaction.on_commit(
partial(
push_edx_modes_from_run, course_run=target_course, edx_client=edx_client
)
)
transaction.on_commit(partial(fix_cloned_run_data, target_course=target_course))
transaction.on_commit(partial(push_edx_modes_from_run, course_run=target_course))


def get_edx_course_modes(
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ dependencies = [
"opentelemetry-sdk>=1.31.0",
"psycopg>=3.2.4,<4",
"psycopg2>=2.9.5,<3",
"pyOpenSSL>=23.1.1,<24",
"pyOpenSSL>=26,<27",
"pycountry>=24.6.1,<25",
"pyparsing>=3.2,<4",
"redis>=5.0.0,<6",
Expand Down
Loading
Loading