- ID
- 82733383-94bd-4f12-99d1-53c9ae476a62
Flat Cookbook List API
Context
V3 cookbook list endpoint currently returns deeply nested data — ~sections~ (containing
~items~ with ~pairing_id~/~product_id~), a full ~author~ object, and ~collections~ objects.
~CookbookSection~ and ~CookbookCollection~ data models are deprecated. Frontend only needs
flat ID lists from the list view.
Goal: remove nesting from the list endpoint. Detail endpoint and underlying data models
stay unchanged.
Response Shape Change
Before
#+begin_src json
{
"id": 1,
"slug": "...",
"author": {"id": 42, "name": "...", "slug": "...", ...},
"collections": [{"id": 1, "name": "...", ...}],
"sections": [
{
"id": 10,
"name": "...",
"position": 0,
"items": [
{"id": "pairing-101", "item_type": "pairing", "pairing_id": 101, "product_id": null},
{"id": "product-201", "item_type": "product", "pairing_id": null, "product_id": 201}
]
}
],
"pairing_count": 1,
"product_count": 1
}
#+end_src
After
Public/Thematic cookbook:
#+begin_src json
{
"id": 1,
"slug": "summer-grilling",
"name": "Summer Grilling",
"description": "Great recipes for the grill.",
"hero_image": "https://cdn.example.com/images/summer-grilling.jpg",
"is_featured": false,
"is_active": true,
"sort_order": 0,
"cookbook_type": "MARKETING",
"visibility": "SHARED",
"owner_id": null,
"author_id": 42,
"dietary_tag_ids": [3, 7],
"pairing_ids": [101, 102],
"product_ids": [201],
"pairing_count": 2,
"product_count": 1,
"referral_code": "",
"count": 50,
"next": "https://api.example.com/api/v3/cookbooks/?page=2",
"previous": null
}
#+end_src
Shared/Favorites cookbook:
#+begin_src json
{
"id": 1,
"slug": "xjK32jal",
"name": "Colin's Favorites",
"description": "",
"hero_image": "https://cdn.example.com/images/summer-grilling.jpg",
"is_featured": false,
"is_active": true,
"sort_order": 0,
"cookbook_type": "FAVORITES",
"visibility": "SHARED",
"author_id": null,
"dietary_tag_ids": [3, 7],
"pairing_ids": [101, 102],
"product_ids": [201],
"pairing_count": 2,
"product_count": 1,
"referral_code": "customer-referral-code",
"count": 50,
"next": "https://api.example.com/api/v3/cookbooks/?page=2",
"previous": null
}
#+end_src
Fields removed: ~author~ (nested), ~sections~ (nested), ~collections~ (nested).
Fields added: ~author_id~ (nullable int), ~pairing_ids~ (flat list), ~product_ids~ (flat list).
Implementation Plan
1. New serializer — ~cookbooks/serializers.py~
Add ~CookbookListSerializer~ after existing ~CookbookSerializer~.
List view gets new serializer; detail view keeps ~CookbookSerializer~ unchanged.
#+begin_src python
class CookbookListSerializer(serializers.Serializer):
# all scalar fields from CookbookSerializer
# (id, slug, name, description, hero_image, is_featured, is_active,
# sort_order, cookbook_type, visibility, kind, owner_id,
# dietary_tag_ids, pairing_count, product_count, referral_code,
# count, next, previous)
author_id = serializers.IntegerField(source="author.id", allow_null=True, default=None)
pairing_ids = serializers.SerializerMethodField()
product_ids = serializers.SerializerMethodField()
def get_pairing_ids(self, obj) -> list[int]:
return [
item.pairing_id
for section in obj.sections.all()
for item in section.items.all()
if item.pairing_id is not None
]
def get_product_ids(self, obj) -> list[int]:
return [
item.product_id
for section in obj.sections.all()
for item in section.items.all()
if item.product_id is not None
]
#+end_src
Uses already-prefetched ~sections__items~ — no extra queries.
2. Wire up list view — ~cookbooks/views.py~
In ~CookbookListView~:
- ~serializer_class = CookbookListSerializer~ (was ~CookbookSerializer~)
- Drop ~collections~ from ~prefetch_related~ in the list queryset (no longer serialized).
- Keep ~sections__items~ prefetch (still needed for ~pairing_ids~/~product_ids~).
3. Update tests — ~cookbooks/tests/test_api.py~
Update ~CookbookV3ReadAPITestCase~ list tests to assert new shape:
- ~sections~ key absent
- ~author_id~ present (int or null), ~author~ key absent
- ~collections~ key absent
- ~pairing_ids~ is a flat list of ints
- ~product_ids~ is a flat list of ints
- Existing filter/count tests that don't inspect shape still pass
Verification
#+begin_src bash
python manage.py test cookbooks.tests.test_api.CookbookV3ReadAPITestCase
#+end_src
Manual check:
#+begin_src bash
curl http://localhost:8000/api/v3/cookbooks/ | python -m json.tool | grep -E 'author|section|collection|pairing_ids|product_ids'
#+end_src
Expect: ~author_id~, ~pairing_ids~, ~product_ids~ present; ~author~, ~sections~, ~collections~ absent.