Skip to content

API versioning demo#3267

Open
mbertrand wants to merge 3 commits into
mainfrom
mb/api_versioning_v2
Open

API versioning demo#3267
mbertrand wants to merge 3 commits into
mainfrom
mb/api_versioning_v2

Conversation

@mbertrand
Copy link
Copy Markdown
Member

@mbertrand mbertrand commented Apr 29, 2026

What are the relevant tickets?

Related to https://github.com/mitodl/hq/issues/5895

Description (What does it do?)

Incorporates the ol-django api_versioning app and adds a demo v2 version of the learning_resources API endpoint with a few sample transformations.

Primarily for testing purposes.

How can this be tested?

  • checkout the mb/api_versioning_v2 branch of mit-learn
  • run docker compose up --build
  • run ./manage.py backpopulate_ovs_data to fetch a few videos
  • Go to http://open.odl.local:8065/api/v2/videos - each "Video" entity should have a cover_image_url field and each caption in caption_urls should include word_count
  • Go to http://open.odl.local:8065/api/v1/videos/ - each "Video" entity should have a thumbnail_url field and no word_count for caption_urls

Additional Context

The first commit adds support for versioning with transforms.
The second commit implements a demo v2 version of the learning_resources API's with some sample transforms

Copilot AI review requested due to automatic review settings April 29, 2026 19:33
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 29, 2026

OpenAPI Changes

75 changes: 0 error, 0 warning, 75 info

View full changelog

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

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

Adds API versioning support (via mitol-django-api-versioning) and introduces a demo v2 namespace/spec for the Learning Resources APIs, including a handful of example breaking changes handled via serializer/version transforms and updated generated TypeScript clients.

Changes:

  • Integrates mitol.api_versioning (settings + serializer mixin usage across multiple apps) and enables v2 in DRF allowed versions.
  • Adds v2 API routing + DRF Spectacular schema endpoints/spec + OpenAPI client generation outputs for v2.
  • Introduces learning_resources v2 demo transformations (department channel_urlurl, video cover_image_urlthumbnail_url, etc.) and updates tests accordingly.

Reviewed changes

Copilot reviewed 22 out of 32 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
articles/serializers.py Adds VersionedSerializerMixin to serializers and improves UserSerializer docstring for OpenAPI.
channels/serializers.py Adds VersionedSerializerMixin to channel-related serializers.
drf_lint_baseline.json Updates baseline line references due to serializer edits.
frontends/api/src/generated/v1/api.ts Regenerated v1 client output (docstring text changes).
frontends/api/src/generated/v2/.openapi-generator/FILES Adds OpenAPI generator metadata for v2 client output.
frontends/api/src/generated/v2/.openapi-generator/VERSION Records OpenAPI generator version used for v2 output.
frontends/api/src/generated/v2/api.ts Adds generated v2 TypeScript Axios client (new output).
frontends/api/src/generated/v2/base.ts Adds generated v2 base client utilities.
frontends/api/src/generated/v2/common.ts Adds generated v2 common request helpers.
frontends/api/src/generated/v2/configuration.ts Adds generated v2 client configuration.
frontends/api/src/generated/v2/index.ts Adds generated v2 entrypoint exports.
learning_resources/serializers.py Applies versioned serializer mixin broadly and introduces v2 demo field changes (department url, run enrollment_url, video thumbnail_url, caption word_count).
learning_resources/serializers_test.py Updates serializer tests to validate both v1/v2 behavior via request version context.
learning_resources/transforms/__init__.py Exposes v2 transforms module for transform registration/discovery.
learning_resources/transforms/v2.py Implements demo v2 transforms for response/request/schema compatibility.
learning_resources/urls.py Adds api/v2/ URL namespace (currently reusing v1 route set).
learning_resources/views_test.py Updates API tests to exercise both v1 and v2 endpoints and versioned serializer output.
learning_resources_search/serializers.py Applies versioning to PercolateQuerySerializer and transforms search hit sources backward per request version.
learning_resources_search/urls.py Adds api/v2/ URL namespace (currently reusing v1 route set).
main/settings.py Installs mitol.api_versioning app and enables v2 in ALLOWED_VERSIONS.
news_events/serializers.py Adds VersionedSerializerMixin to serializers.
openapi/settings_spectacular.py Adds versioned schema post-processing hook.
openapi/specs/v1.yaml Regenerated OpenAPI v1 spec (adds missing schema descriptions).
openapi/urls.py Adds /api/v2/schema/* endpoints for v2 schema + docs.
profiles/serializers.py Adds VersionedSerializerMixin to serializers.
pyproject.toml Adds mitol-django-api_versioning dependency and uv git source reference.
scripts/generate_openapi.sh Generates v2 TypeScript client alongside v0/v1.
scripts/openapi-configs/typescript-axios-v2.yaml Adds OpenAPI generator config for v2 TypeScript client output.
testimonials/serializers.py Adds VersionedSerializerMixin to serializer.
uv.lock Locks new dependency and its source.
video_shorts/serializers.py Adds VersionedSerializerMixin to serializers.

Comment thread learning_resources/serializers.py Outdated
Comment thread learning_resources/serializers.py Outdated
Comment thread pyproject.toml Outdated
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

Copilot reviewed 22 out of 32 changed files in this pull request and generated 2 comments.

Comments suppressed due to low confidence (1)

learning_resources/serializers.py:1384

  • ContentFileSerializer now participates in API versioning, but several serializer method fields instantiate other serializers without passing through self.context (e.g., departments/topics/platform/offered_by). With versioned transforms (like LearningResourceDepartmentSerializer renaming url/channel_url), this can cause v1 responses to emit the v2 shape because the nested serializers don’t see request.version. Pass context=self.context when constructing nested serializers (and similarly in any callers that manually instantiate ContentFileSerializer).
class ContentFileSerializer(VersionedSerializerMixin, serializers.ModelSerializer):
    """
    Serializer class for course run ContentFiles
    """

Comment thread learning_resources/views_test.py Outdated
Comment on lines +412 to +416
enrollment_url = serializers.SerializerMethodField()

def get_enrollment_url(self, instance) -> str | None:
"""Return a URL for enrollment, derived from the run URL."""
return f"{instance.url.rstrip('/')}/enroll/" if instance.url else None
Copy link

Copilot AI Apr 30, 2026

Choose a reason for hiding this comment

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

The new enrollment_url field behavior (including the exact formatting and versioning behavior—present in v2, removed/absent in v0/v1 via transforms) isn’t covered by tests. Add serializer/view tests that assert enrollment_url is correctly derived from run.url in v2 and is not present for v1.

Copilot uses AI. Check for mistakes.
Co-authored-by: Copilot <copilot@github.com>
@mbertrand mbertrand force-pushed the mb/api_versioning_v2 branch 4 times, most recently from 862ba9e to 828f2d2 Compare April 30, 2026 13:47
@mbertrand mbertrand requested a review from Copilot April 30, 2026 13:47
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

Copilot reviewed 24 out of 34 changed files in this pull request and generated 3 comments.

Comments suppressed due to low confidence (1)

learning_resources/serializers.py:1389

  • Now that ContentFileSerializer (and several nested serializers it calls) are version-aware via VersionedSerializerMixin, the manual serializer instantiations inside ContentFileSerializer should pass through self.context (or at least the request). For example, get_departments currently constructs LearningResourceDepartmentSerializer(...).data without context, which will likely emit the v2 field name (url) even on v1 endpoints because the nested serializer can’t see request.version.
class ContentFileSerializer(VersionedSerializerMixin, serializers.ModelSerializer):
    """
    Serializer class for course run ContentFiles
    """

hits = instance.get("hits", {}).get("hits", [])
return (hit.get("_source") for hit in hits)
return (
transform_dict_backwards(hit.get("_source"), ContentFileSerializer, request)
Copy link

Copilot AI Apr 30, 2026

Choose a reason for hiding this comment

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

ContentFileSearchResponseSerializer.get_results calls transform_dict_backwards(...) without recursive=True, while the learning-resources variant explicitly enables recursion. If transform_dict_backwards is non-recursive by default, nested structures in ContentFileSerializer output (e.g., departments items where v2 uses url and v1 expects channel_url) won’t be transformed for older API versions. Consider enabling recursive transforms here as well so nested renamed fields are handled consistently.

Suggested change
transform_dict_backwards(hit.get("_source"), ContentFileSerializer, request)
transform_dict_backwards(
hit.get("_source"),
ContentFileSerializer,
request,
recursive=True,
)

Copilot uses AI. Check for mistakes.
Comment on lines +521 to 525
def get_word_count(self, obj) -> int:
url = obj.get("url", "") if isinstance(obj, dict) else ""
return random.Random(url).randint(100, 5000) # noqa: S311


Copy link

Copilot AI Apr 30, 2026

Choose a reason for hiding this comment

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

CaptionUrlSerializer.get_word_count generates a pseudo-random value (and suppresses S311) instead of deriving a real word count. This means the API can return arbitrary values that don’t reflect caption content, and it will still return a number even if the caption entry has no url (seed becomes an empty string). Consider either computing the word count from actual caption text/metadata, or (if this is only a demo field) returning None/omitting the field when inputs are missing and avoiding random + the security lint suppression.

Suggested change
def get_word_count(self, obj) -> int:
url = obj.get("url", "") if isinstance(obj, dict) else ""
return random.Random(url).randint(100, 5000) # noqa: S311
def get_word_count(self, obj) -> int | None:
"""Return the number of words in caption text when it is available."""
if not isinstance(obj, dict):
return None
caption_text = None
for key in ("text", "content", "caption", "transcript"):
value = obj.get(key)
if isinstance(value, str) and value.strip():
caption_text = value
break
if caption_text is None:
return None
return len(caption_text.split())

Copilot uses AI. Check for mistakes.
Comment on lines +119 to +124
if "thumbnail_url" in data:
data["cover_image_url"] = data["thumbnail_url"]
elif instance is not None:
data["cover_image_url"] = getattr(instance, "cover_image_url", "")
else:
data["cover_image_url"] = ""
Copy link

Copilot AI Apr 30, 2026

Choose a reason for hiding this comment

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

VideoRemoveCoverImageUrl.to_representation falls back to setting cover_image_url to an empty string when there’s no thumbnail_url and no instance. This produces a value that doesn’t match the schema (nullable URI) and can leak into cases where transforms are applied to dict data without a model instance (e.g., search results). Prefer None as the fallback and avoid relying on transform ordering to ensure thumbnail_url is still present when deriving cover_image_url.

Suggested change
if "thumbnail_url" in data:
data["cover_image_url"] = data["thumbnail_url"]
elif instance is not None:
data["cover_image_url"] = getattr(instance, "cover_image_url", "")
else:
data["cover_image_url"] = ""
thumbnail_url = data.get("thumbnail_url")
if thumbnail_url is None and instance is not None:
thumbnail_url = getattr(instance, "thumbnail_url", None)
if thumbnail_url is not None:
data["cover_image_url"] = thumbnail_url
elif instance is not None:
data["cover_image_url"] = getattr(instance, "cover_image_url", None)
else:
data["cover_image_url"] = None

Copilot uses AI. Check for mistakes.
@mbertrand mbertrand force-pushed the mb/api_versioning_v2 branch from 828f2d2 to 64cc7c7 Compare May 1, 2026 01:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants