Skip to content

Route drawer CTAs for MITxOnline programs-as-courses to correct product pages#3063

Open
ChristopherChudzicki wants to merge 11 commits intomainfrom
cc/drawer-links
Open

Route drawer CTAs for MITxOnline programs-as-courses to correct product pages#3063
ChristopherChudzicki wants to merge 11 commits intomainfrom
cc/drawer-links

Conversation

@ChristopherChudzicki
Copy link
Copy Markdown
Contributor

@ChristopherChudzicki ChristopherChudzicki commented Mar 17, 2026

What are the relevant tickets?

Closes https://github.com/mitodl/hq/issues/10538

Note

The vast majority of the line changes here are auto-generated files (e.g., openapi docstring changes)

Description (What does it do?)

When the MITxOnline product pages feature flag is enabled, search drawer CTA links for MITxOnline resources now route to the correct product page based on resource_type_group:

  • Courses/courses/{readable_id}
  • Programs displayed as courses/courses/p/{readable_id}
  • Programs/programs/{readable_id}

Previously, all MITxOnline programs linked to /programs/{readable_id} because display_mode was hardcoded to null.

Implementation overview

  • Backend serializers: Refactored resource_type_group from a SerializerMethodField (typed as string in OpenAPI) to a ComputedResourceTypeGroupField backed by ResourceTypeGroupChoiceField. This causes drf-spectacular to generate a proper ResourceTypeGroupEnum instead of a plain string, giving the frontend typed values to branch on.
  • Drawer CTA: getResourceUrl in CallToActionSection.tsx now checks resource.resource_type_group === ResourceTypeGroupEnum.Course to pass DisplayModeEnum.Course to programPageView, routing program-as-course resources to /courses/p/.
  • Test factories: All resource type factories now set resource_type_group to their appropriate enum value.
  • Test coverage: Added a test case for the program-as-course drawer CTA routing.

How can this be tested?

Prerequisites:

  • Run MITxOnline ETL against RC or Local (./manage.py backpopulate_mitxonline_data). Ensure the MITxOnline environment has some courses, programs, and separate programs with display_mode="course"
  • mitxonline-product-pages feature flag in PostHog turned on
  1. Program As Course Test: In /search page, search for an MITxOnline program that has display_mode="course"; Open the drawer, click "Learn More". You should end up at /courses/p/{readable_id}.
  2. Standard Program: Repeat (1) for a standard program. The "Learn more" Link should go to /programs/{readable_id}
    • /search?resource_type_group=program&platform=mitxonline
  3. Standard Courses: Repeat (1) for a course. The "Learn more" link should go to /courses/{readable_id}.
    • /search?resource_type_group=course&resource_type=course&platform=mitxonline

Copilot AI review requested due to automatic review settings March 17, 2026 19:03
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR refines the resource_type_group concept across the backend API, OpenAPI specs, and frontend, so clients can reliably treat it as a first-class enum and correctly route “program displayed as course” content on MITxOnline product pages.

Changes:

  • Introduces reusable DRF fields for resource_type_group, including a computed, read-only serializer field for learning resources.
  • Updates OpenAPI specs (v0/v1) to document resource_type_group more clearly and represent it as an enum (including adding ResourceTypeGroupEnum to v0).
  • Updates frontend routing + tests + factories to handle MITxOnline “program-as-course” links using resource_type_group.

Reviewed changes

Copilot reviewed 8 out of 10 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
openapi/specs/v1.yaml Updates descriptions and schema typing for resource_type_group across query params and component schemas.
openapi/specs/v0.yaml Adds ResourceTypeGroupEnum and updates resource_type_group fields to reference it.
learning_resources/serializers.py Adds ResourceTypeGroupChoiceField and ComputedResourceTypeGroupField; replaces SerializerMethodField usage.
learning_resources/constants.py Adds RESOURCE_TYPE_GROUP_CHOICES derived from RESOURCE_TYPE_GROUP_VALUES for DRF choice fields.
learning_resources_search/serializers.py Reuses the shared ResourceTypeGroupChoiceField for the search request serializer.
frontends/main/src/page-components/LearningResourceExpanded/CallToActionSection.tsx Routes MITxOnline programs to course-style pages when resource_type_group indicates “course”.
frontends/main/src/page-components/LearningResourceExpanded/CallToActionSection.test.tsx Adds coverage for the MITxOnline “program-as-course” routing behavior under the feature flag.
frontends/api/src/test-utils/factories/learningResources.ts Ensures factory resources include resource_type_group to match the new generated typings.
frontends/api/src/generated/v1/api.ts Updates generated types so resource_type_group is ResourceTypeGroupEnum instead of string.
frontends/api/src/generated/v0/api.ts Adds ResourceTypeGroupEnum and updates generated types accordingly.

You can also share your feedback on Copilot code review. Take the survey.

@github-actions
Copy link
Copy Markdown

github-actions bot commented Mar 20, 2026

OpenAPI Changes

624 changes: 0 error, 624 warning, 0 info

View full changelog

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

--volume ${{ github.workspace }}:${{ github.workspace }}:rw \
-e GITHUB_WORKSPACE=${{ github.workspace }} \
tufin/oasdiff changelog --composed \
tufin/oasdiff changelog --composed --flatten-allof \
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

See https://github.com/oasdiff/oasdiff/blob/main/docs/ALLOF.md, -flatten-allof helps reduce some false positives

```
${{ steps.oasdif_changelog.outputs.changelog }}
```
[View full changelog](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

link to the details instead of posting all of the changelog... in this case, it was bigger than the comment could handle

@pdpinch
Copy link
Copy Markdown
Member

pdpinch commented Mar 24, 2026

Would this be a good PR to make the change to open the product page in the same tab, instead of opening a new one?

@ChristopherChudzicki ChristopherChudzicki added the Needs Review An open Pull Request that is ready for review label Mar 24, 2026
readable_id: resource.readable_id,
display_mode: null,
display_mode:
resource.resource_type_group === ResourceTypeGroupEnum.Course
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

ResourceTypeGroupEnum.Course and DisplayModeEnum.Course both happen to be "course". The code works, but coupling two enums from different domains via string coincidence is fragile. If either value changes, this silently breaks.

Suggestion: Either pass resource.resource_type_group directly as display_mode (since the values align by design), or add a comment explicitly calling out that these values are intentionally the same. Could also be simplified by avoiding DisplayModeEnum entirely here since programPageView just checks for the string "course":

display_mode:
  resource.resource_type_group === ResourceTypeGroupEnum.Course
    ? resource.resource_type_group  // "course" satisfies BaseProgramDisplayMode
    : undefined,

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Added a comment about the data.

{ resource_type: ResourceTypeEnum.Program },
{
resource_type: ResourceTypeEnum.Program,
resource_type_group: faker.helpers.arrayElement([
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

resource_type_group: faker.helpers.arrayElement([
  ResourceTypeGroupEnum.Program,
  ResourceTypeGroupEnum.Course,  // random!
]),

Randomly picking between Program and Course is realistic but can cause flaky tests if any test renders a program CTA without explicitly overriding resource_type_group. Tests that care about program routing now need to pin this value.

Suggestion: This is probably fine given that the factory comment explains the rationale, but worth making sure all existing program-related tests that check CTA links explicitly set resource_type_group.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Programs can have either resource_type_group, so our data factories should support both.

I think it's true that this could cause a flakey test. On the other hand, that's also true of how we create randomized test data for all our tests (on the frontend and backend) via faker (frontend) or faker+factory-boy (backend).

A similar example is learningResource(overrides), which creates a random learning resource of any type.

The idea is that if resource_type_group matters for a specific test, the test writer should pin the value

learningResource({ resource_type: "program", resource_type_group: "whatever-specific-value"})


def __init__(self, **kwargs):
kwargs.setdefault("read_only", True)
kwargs.setdefault("source", "*")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The base class ResourceTypeGroupChoiceField is exported and reused in the search serializer — good. The subclass ComputedResourceTypeGroupField cleanly separates the "what values are valid" concern from the "how to compute the value" concern. This is a nice design.

One minor nit: source="*" on ComputedResourceTypeGroupField is a somewhat obscure DRF pattern. A short comment explaining why it's needed (to pass the full instance to to_representation) would help future readers.

@ahtesham-quraish
Copy link
Copy Markdown
Contributor

Overall Looks good to me. The only thing which I would suggest is the detail level testing which I am sure you would have done.

ChristopherChudzicki and others added 11 commits March 28, 2026 16:19
…API enum generation

Extract ResourceTypeGroupChoiceField and ComputedResourceTypeGroupField from
the inline SerializerMethodField so that drf-spectacular generates a proper
ResourceTypeGroupEnum instead of a plain string.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
resource_type_group is now typed as ResourceTypeGroupEnum instead of string.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…urses/p/

When the MITxOnline product pages feature flag is enabled, programs with
resource_type_group="course" now link to /courses/p/{readable_id} instead of
/programs/{readable_id}. Also adds resource_type_group to test factories and
test coverage for the program-as-course routing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Needs Review An open Pull Request that is ready for review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants