Skip to content

Include parent programs in ProgramSerializer#3391

Merged
cp-at-mit merged 5 commits intomainfrom
10432-when-a-program-is-a-requirement-of-another-expose-its-parent-programs
Mar 19, 2026
Merged

Include parent programs in ProgramSerializer#3391
cp-at-mit merged 5 commits intomainfrom
10432-when-a-program-is-a-requirement-of-another-expose-its-parent-programs

Conversation

@cp-at-mit
Copy link
Contributor

@cp-at-mit cp-at-mit commented Mar 17, 2026

What are the relevant tickets?

https://github.com/mitodl/hq/issues/10432

Description (What does it do?)

Add support for including parent (required) programs when serializing Program objects. Introduces a programs SerializerMethodField and get_programs implementation that mirrors CourseSerializer behavior, uses BaseProgramSerializer for output, and filters parents by org_id/contract_id or excludes B2B-only programs. ProgramViewSet.get_serializer_context now sets include_programs and passes org_id/contract_id from query params to enable this behavior for single-item retrievals and readable_id lookups. Tests updated and a new test added to assert parent programs are returned (or null when not requested).

How can this be tested?

Create a program. Add that program as a requirement for another program. Call the http://mitxonline.odl.local:8013/api/v2/programs/2/ for the first program and verify that the second program is listed in the "programs" list.

Add support for including parent (required) programs when serializing Program objects. Introduces a programs SerializerMethodField and get_programs implementation that mirrors CourseSerializer behavior, uses BaseProgramSerializer for output, and filters parents by org_id/contract_id or excludes B2B-only programs. ProgramViewSet.get_serializer_context now sets include_programs and passes org_id/contract_id from query params to enable this behavior for single-item retrievals and readable_id lookups. Tests updated and a new test added to assert parent programs are returned (or null when not requested).
@github-actions
Copy link

github-actions bot commented Mar 17, 2026

OpenAPI Changes

Show/hide ## Changes for v0.yaml:
## Changes for v0.yaml:
8 changes: 0 error, 0 warning, 8 info
info	[response-required-property-added] at head/openapi/specs/v0.yaml	
	in API GET /api/v2/pages/?fields=*&type=cms.programpage
		added the required property 'items/items/program_details/programs' to the response with the '200' status

info	[response-required-property-added] at head/openapi/specs/v0.yaml	
	in API GET /api/v2/pages/{id}/
		added the required property '/oneOf[#/components/schemas/ProgramPageItem]/program_details/programs' to the response with the '200' status

info	[response-required-property-added] at head/openapi/specs/v0.yaml	
	in API GET /api/v2/program_certificates/{cert_uuid}/
		added the required property 'program/programs' to the response with the '200' status

info	[response-required-property-added] at head/openapi/specs/v0.yaml	
	in API GET /api/v2/program_enrollments/
		added the required property '/items/program/programs' to the response with the '200' status

info	[response-required-property-added] at head/openapi/specs/v0.yaml	
	in API DELETE /api/v2/program_enrollments/{id}/
		added the required property '/items/program/programs' to the response with the '200' status

info	[response-required-property-added] at head/openapi/specs/v0.yaml	
	in API GET /api/v2/program_enrollments/{id}/
		added the required property 'program/programs' to the response with the '200' status

info	[response-required-property-added] at head/openapi/specs/v0.yaml	
	in API GET /api/v2/programs/
		added the required property 'results/items/programs' to the response with the '200' status

info	[response-required-property-added] at head/openapi/specs/v0.yaml	
	in API GET /api/v2/programs/{id}/
		added the required property 'programs' to the response with the '200' status



## Changes for v1.yaml:
8 changes: 0 error, 0 warning, 8 info
info	[response-required-property-added] at head/openapi/specs/v1.yaml	
	in API GET /api/v2/pages/?fields=*&type=cms.programpage
		added the required property 'items/items/program_details/programs' to the response with the '200' status

info	[response-required-property-added] at head/openapi/specs/v1.yaml	
	in API GET /api/v2/pages/{id}/
		added the required property '/oneOf[#/components/schemas/ProgramPageItem]/program_details/programs' to the response with the '200' status

info	[response-required-property-added] at head/openapi/specs/v1.yaml	
	in API GET /api/v2/program_certificates/{cert_uuid}/
		added the required property 'program/programs' to the response with the '200' status

info	[response-required-property-added] at head/openapi/specs/v1.yaml	
	in API GET /api/v2/program_enrollments/
		added the required property '/items/program/programs' to the response with the '200' status

info	[response-required-property-added] at head/openapi/specs/v1.yaml	
	in API DELETE /api/v2/program_enrollments/{id}/
		added the required property '/items/program/programs' to the response with the '200' status

info	[response-required-property-added] at head/openapi/specs/v1.yaml	
	in API GET /api/v2/program_enrollments/{id}/
		added the required property 'program/programs' to the response with the '200' status

info	[response-required-property-added] at head/openapi/specs/v1.yaml	
	in API GET /api/v2/programs/
		added the required property 'results/items/programs' to the response with the '200' status

info	[response-required-property-added] at head/openapi/specs/v1.yaml	
	in API GET /api/v2/programs/{id}/
		added the required property 'programs' to the response with the '200' status



## Changes for v2.yaml:
8 changes: 0 error, 0 warning, 8 info
info	[response-required-property-added] at head/openapi/specs/v2.yaml	
	in API GET /api/v2/pages/?fields=*&type=cms.programpage
		added the required property 'items/items/program_details/programs' to the response with the '200' status

info	[response-required-property-added] at head/openapi/specs/v2.yaml	
	in API GET /api/v2/pages/{id}/
		added the required property '/oneOf[#/components/schemas/ProgramPageItem]/program_details/programs' to the response with the '200' status

info	[response-required-property-added] at head/openapi/specs/v2.yaml	
	in API GET /api/v2/program_certificates/{cert_uuid}/
		added the required property 'program/programs' to the response with the '200' status

info	[response-required-property-added] at head/openapi/specs/v2.yaml	
	in API GET /api/v2/program_enrollments/
		added the required property '/items/program/programs' to the response with the '200' status

info	[response-required-property-added] at head/openapi/specs/v2.yaml	
	in API DELETE /api/v2/program_enrollments/{id}/
		added the required property '/items/program/programs' to the response with the '200' status

info	[response-required-property-added] at head/openapi/specs/v2.yaml	
	in API GET /api/v2/program_enrollments/{id}/
		added the required property 'program/programs' to the response with the '200' status

info	[response-required-property-added] at head/openapi/specs/v2.yaml	
	in API GET /api/v2/programs/
		added the required property 'results/items/programs' to the response with the '200' status

info	[response-required-property-added] at head/openapi/specs/v2.yaml	
	in API GET /api/v2/programs/{id}/
		added the required property 'programs' to the response with the '200' status



Unexpected changes? Ensure your branch is up-to-date with main (consider rebasing).

@cp-at-mit cp-at-mit marked this pull request as ready for review March 18, 2026 19:19
Comment on lines +172 to +181
programs_qs = programs_qs.filter(
program__contract_memberships__contract__organization__pk=self.context.get(
"org_id"
)
)
elif self.context.get("contract_id"):
programs_qs = programs_qs.filter(
program__contract_memberships__contract__pk=self.context.get(
"contract_id"
)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: The get_programs method can return duplicate parent programs because the underlying queryset is not deduplicated with .distinct() after joining on one-to-many relationships.
Severity: MEDIUM

Suggested Fix

Add a .distinct() call to the queryset before it is evaluated to ensure each parent program is included only once. For example, programs_qs.select_related("program").distinct().all() would prevent duplicate program objects from being added to the list.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.

Location: courses/serializers/v2/programs.py#L172-L181

Potential issue: The `get_programs` method in the `ProgramSerializer` can return
duplicate parent programs in the API response. This happens when filtering by `org_id`
or `contract_id`. The filtering involves joining across one-to-many relationships like
`program__contract_memberships`. If a program is part of multiple contracts within the
same organization, the underlying queryset will contain duplicate rows. The code then
constructs a list from this queryset without applying `.distinct()`, preserving the
duplicates in the final serialized output.

Did we get this right? 👍 / 👎 to inform future reviews.

@annagav annagav self-requested a review March 19, 2026 13:34
@annagav
Copy link
Contributor

annagav commented Mar 19, 2026

@cp-at-mit how do I make a program required for another program?

Copy link
Contributor

@annagav annagav left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM 👍

@cp-at-mit cp-at-mit merged commit 306127e into main Mar 19, 2026
10 checks passed
@cp-at-mit cp-at-mit deleted the 10432-when-a-program-is-a-requirement-of-another-expose-its-parent-programs branch March 19, 2026 20:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants