Org Web Adapter

hungryroot/popsicle/be-7924-flatten-cookbook-list-phase2.org

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.