Org Web Adapter

hungryroot/popsicle/flat-cookbook-list-api.org

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.