diff --git a/CHANGES/7831.feature b/CHANGES/7831.feature new file mode 100644 index 0000000000..a89d6a70d2 --- /dev/null +++ b/CHANGES/7831.feature @@ -0,0 +1 @@ +Added an opt-in ``content_ids`` field to the repository version detail, returning the list of content unit UUIDs in the version when the request includes the ``content_ids=true`` query parameter. This lets clients diff content sets between versions directly instead of issuing expensive content filter queries. diff --git a/pulpcore/app/serializers/repository.py b/pulpcore/app/serializers/repository.py index 191ed4c991..8dc4c283f7 100644 --- a/pulpcore/app/serializers/repository.py +++ b/pulpcore/app/serializers/repository.py @@ -291,10 +291,26 @@ class RepositoryVersionSerializer(ModelSerializer, NestedHyperlinkedModelSeriali vuln_report = serializers.SerializerMethodField( read_only=True, ) + content_ids = serializers.SerializerMethodField( + help_text=_( + "The list of content unit UUIDs in this version. Only returned when the request " + "includes the 'content_ids=true' query parameter; otherwise null." + ), + read_only=True, + ) def get_vuln_report(self, object): return f"{reverse('vuln_report-list')}?repo_versions={get_prn(object)}" + def get_content_ids(self, object): + request = self.context.get("request") + if request is None: + return None + value = request.query_params.get("content_ids") + if value is None or value.lower() not in ("true", "1", "yes"): + return None + return object.content_ids + class Meta: model = models.RepositoryVersion fields = ModelSerializer.Meta.fields + ( @@ -304,6 +320,7 @@ class Meta: "base_version", "content_summary", "vuln_report", + "content_ids", ) diff --git a/pulpcore/tests/unit/serializers/test_repository.py b/pulpcore/tests/unit/serializers/test_repository.py index 1c7adc2afa..4b47bbd2d2 100644 --- a/pulpcore/tests/unit/serializers/test_repository.py +++ b/pulpcore/tests/unit/serializers/test_repository.py @@ -1,5 +1,6 @@ from types import SimpleNamespace from unittest.mock import Mock +from uuid import uuid4 import pytest from rest_framework import serializers @@ -10,6 +11,7 @@ PublicationSerializer, RemoteSerializer, RepositorySyncURLSerializer, + RepositoryVersionSerializer, ValidateFieldsMixin, ) @@ -265,3 +267,36 @@ def test_create_remote_with_invalid_parameter(): serializer = RemoteSerializer(data=data) with pytest.raises(serializers.ValidationError, match="Unexpected field"): serializer.validate(data) + + +def _content_ids_serializer(query_params=None): + """Build a RepositoryVersionSerializer with an optional fake request in its context.""" + context = {} + if query_params is not None: + context["request"] = SimpleNamespace(query_params=query_params) + return RepositoryVersionSerializer(context=context) + + +def test_get_content_ids_omitted_without_request(): + """content_ids is null when there is no request in the serializer context.""" + obj = SimpleNamespace(content_ids=[uuid4()]) + serializer = _content_ids_serializer() + assert serializer.get_content_ids(obj) is None + + +@pytest.mark.parametrize("value", [None, "", "false", "0", "no", "False"]) +def test_get_content_ids_omitted_when_not_requested(value): + """content_ids is null unless the request explicitly opts in via the query parameter.""" + query_params = {} if value is None else {"content_ids": value} + obj = SimpleNamespace(content_ids=[uuid4()]) + serializer = _content_ids_serializer(query_params=query_params) + assert serializer.get_content_ids(obj) is None + + +@pytest.mark.parametrize("value", ["true", "True", "TRUE", "1", "yes"]) +def test_get_content_ids_returned_when_requested(value): + """content_ids returns the version's UUIDs when the request opts in.""" + content_ids = [uuid4(), uuid4()] + obj = SimpleNamespace(content_ids=content_ids) + serializer = _content_ids_serializer(query_params={"content_ids": value}) + assert serializer.get_content_ids(obj) == content_ids