- ID
- c1da96b3-9939-4015-85c2-4ade0932e731
BE-7924: Flatten Cookbook LIST endpoint - Phase 2: remove author, sections, collections
Context
Phase 1 (BE-7922) added ~author_id~, ~pairing_ids~, ~product_ids~ to ~CookbookListSerializer~.
Clients now use those flat fields. Phase 2 removes the old nested fields (~author~,
~sections~, ~collections~) from the list response.
The removal is gated behind ~BoolDynamicConfigEnum.COOKBOOK_LIST_REMOVE_DEPRECATED_FIELDS~
(default on), so it can be toggled off for a rollback without a deploy.
Detail endpoint (~CookbookSerializer~) is untouched.
See: [[https://hungryroot.atlassian.net/browse/BE-7924][BE-7924]]
Response Shape Change
Before (list response still included nested fields from CookbookSerializer parent)
#+begin_src json
{
"id": 1,
"slug": "summer-grilling",
"author": {"id": 42, "name": "...", "slug": "...", ...},
"sections": [{"id": 10, "name": "...", "items": [...]}],
"collections": [{"id": 1, "slug": "...", ...}],
"author_id": 42,
"pairing_ids": [101, 102],
"product_ids": [201]
}
#+end_src
After (flag on / default)
#+begin_src json
{
"id": 1,
"slug": "summer-grilling",
"author_id": 42,
"pairing_ids": [101, 102],
"product_ids": [201]
}
#+end_src
Fields removed: ~author~ (nested object), ~sections~ (list), ~collections~ (list).
Fields retained: ~author_id~, ~pairing_ids~, ~product_ids~ plus all other scalar fields.
Implementation Plan
1. Add DynamicConfig flag — ~app/constants/dynamic_config.py~
Add to ~BoolDynamicConfigEnum~:
#+begin_src python
COOKBOOK_LIST_REMOVE_DEPRECATED_FIELDS = (
"api.cookbooks.list.remove_deprecated_fields",
"When enabled, removes author, sections, and collections from GET /api/v3/cookbooks/ responses. "
"Disable to roll back while client migration is in progress.",
"1",
)
#+end_src
Default ~"1"~ = on (new flat-only behavior active by default).
2. Remove deprecated fields in CookbookListSerializer — ~cookbooks/serializers.py~
Override ~to_representation~ to pop the three legacy keys when the flag is active:
#+begin_src python
from app.constants.dynamic_config import BoolDynamicConfigEnum
from app.services.shared.dynamic_config import bool_config_is_active
class CookbookListSerializer(CookbookSerializer):
author_id = serializers.SerializerMethodField()
pairing_ids = serializers.SerializerMethodField()
product_ids = serializers.SerializerMethodField()
def to_representation(self, instance):
data = super().to_representation(instance)
if bool_config_is_active(BoolDynamicConfigEnum.COOKBOOK_LIST_REMOVE_DEPRECATED_FIELDS):
data.pop("author", None)
data.pop("sections", None)
data.pop("collections", None)
return data
# existing get_author_id / get_pairing_ids / get_product_ids unchanged
#+end_src
3. Drop collections prefetch in list queryset — ~cookbooks/views.py~
Modify ~_get_cookbooks_queryset~ to accept ~prefetch_collections~ kwarg:
#+begin_src python
def _get_cookbooks_queryset(*, active_only: bool, prefetch_collections: bool = True) -> QuerySet[models.Cookbook]:
prefetches: list = [
"dietary_tags",
Prefetch("sections", queryset=_get_sections_queryset(active_only=active_only)),
]
if prefetch_collections:
prefetches.append(
Prefetch("collections", queryset=models.CookbookCollection.objects.order_by("sort_order", "id"))
)
queryset = (
models.Cookbook.objects.select_related("author")
.prefetch_related(*prefetches)
.annotate(...)
.order_by("sort_order", "id")
.distinct()
)
if active_only:
queryset = queryset.filter(is_active=True)
return queryset
#+end_src
In ~CookbookListView.get_queryset()~, pass the flag:
#+begin_src python
def get_queryset(self):
include_inactive = utils.is_param_truthy(self.request.query_params.get("include_inactive", False))
active_only = not include_inactive
prefetch_collections = not bool_config_is_active(BoolDynamicConfigEnum.COOKBOOK_LIST_REMOVE_DEPRECATED_FIELDS)
return (
_get_cookbooks_queryset(active_only=active_only, prefetch_collections=prefetch_collections)
.exclude(visibility=models.CookbookVisibility.PRIVATE)
.exclude(cookbook_type=models.CookbookType.FAVORITES)
)
#+end_src
4. Update tests — ~cookbooks/tests/test_api.py~
Add to ~CookbookV3ReadAPITestCase~:
#+begin_src python
def test_cookbook_list_excludes_deprecated_nested_fields_by_default(self):
response = self.client.get(reverse(self.cookbook_list_route_name))
self.assertEqual(response.status_code, status.HTTP_200_OK)
result = response.json()["results"][0]
self.assertNotIn("author", result)
self.assertNotIn("sections", result)
self.assertNotIn("collections", result)
def test_cookbook_list_includes_deprecated_fields_when_flag_disabled(self):
from app.tests.helper.dynamic_config import override_dynamic_config
with override_dynamic_config(BoolDynamicConfigEnum.COOKBOOK_LIST_REMOVE_DEPRECATED_FIELDS, value=False):
response = self.client.get(reverse(self.cookbook_list_route_name))
self.assertEqual(response.status_code, status.HTTP_200_OK)
result = response.json()["results"][0]
self.assertIn("author", result)
self.assertIn("sections", result)
self.assertIn("collections", result)
#+end_src
Verification
#+begin_src bash
python manage.py test cookbooks.tests.test_api.CookbookV3ReadAPITestCase --keepdb
#+end_src
Manual check (flag on by default):
#+begin_src bash
curl http://localhost:8000/api/v3/cookbooks/ | python -m json.tool | python -c "
import json, sys
data = json.load(sys.stdin)
r = data['results'][0]
print('author present:', 'author' in r)
print('sections present:', 'sections' in r)
print('collections present:', 'collections' in r)
print('author_id present:', 'author_id' in r)
"
#+end_src
Expect: ~author~, ~sections~, ~collections~ absent; ~author_id~, ~pairing_ids~, ~product_ids~ present.