Org Web Adapter

hungryroot/brand/cookbook-dietary-tag-backfill.org

ID
6239515d-8f03-48df-9835-8e7e04839335

Cookbook Dietary Tag Backfill

One-off script to populate =Cookbook.dietary_tags= (M2M to =app.Tag=) by unioning,

across every pairing and product referenced from the cookbook's sections:

- the =HAS_X= allergen tags (IDs 66-75), computed from the =has_*= boolean fields, and

- the protein tags (tag groups =MEAT_PROTEIN=, =SEAFOOD_PROTEIN=, =PLANT_PROTEIN=),

sourced directly from each item's existing =tags= M2M.

Tag IDs and exclusion semantics follow the Confluence doc

[[https://hungryroot.atlassian.net/wiki/spaces/WD/pages/1597276176/Discover+Cookbooks+Filter+to+API+Mapping][Discover Cookbooks - Filter to API Mapping]]:

- =/api/v3/cookbooks/= filters protein via =tag_id= against this set

and filters dietary restrictions via =exclude_tag_id= against the =HAS_*= subset

(e.g. Vegan -> =exclude_tag_id=66,67,74,69,72,75=).

- VEGAN/GLUTEN_FREE badge tags are still NOT written here.

Usage

1. SSH to server, =python manage.py shell=.

2. Paste the script below.

3. With =DRY_RUN=True= (default) it prints =existing= / =computed= / =adding= per cookbook

without writing.

4. Optionally set =COOKBOOK_ID= to a single cookbook id to scope the run.

5. When the diff looks right, flip =DRY_RUN=False= and paste again.

Existing tags are preserved; the script only adds missing =HAS_*= tag IDs (merge, not replace).

Script

#SRC_BEGIN python

from django.db import transaction

from cookbooks.models import Cookbook, CookbookDietaryTag, CookbookItem, CookbookItemType

from app.models.tag import Tag

from app.models.tag_group import TagGroup

# Set to a single Cookbook ID to test on one; leave None to process all.

COOKBOOK_ID = None

DRY_RUN = True # Flip to False to actually write.

# has_* field on DietaryMixin -> Tag.ID for the HAS_X allergen tag.

# Mirrors exclude_tag_id sets in the Confluence "Discover Cookbooks - Filter to API Mapping" doc.

HAS_FIELD_TO_TAG_ID = {

"has_meat": Tag.ID.HAS_MEAT, # 66

"has_fish": Tag.ID.HAS_FISH, # 67

"has_gluten": Tag.ID.HAS_GLUTEN, # 68

"has_dairy": Tag.ID.HAS_DAIRY, # 69

"has_soy": Tag.ID.HAS_SOY, # 70

"has_nut": Tag.ID.HAS_NUT, # 71

"has_egg": Tag.ID.HAS_EGG, # 72

"has_peanut": Tag.ID.HAS_PEANUT, # 73

"has_byproduct": Tag.ID.HAS_BYPRODUCT, # 74

"has_shellfish": Tag.ID.HAS_SHELLFISH, # 75

}

PROTEIN_TAG_GROUP_IDS = {

TagGroup.ID.MEAT_PROTEIN, # 1

TagGroup.ID.SEAFOOD_PROTEIN, # 2

TagGroup.ID.PLANT_PROTEIN, # 3

}

def allergen_tag_ids(obj) -> set[int]:

"""Return HAS_X tag IDs for any allergen the obj contains."""

return {tag_id for field, tag_id in HAS_FIELD_TO_TAG_ID.items() if getattr(obj, field)}

def protein_tag_ids(obj) -> set[int]:

"""Return protein Tag IDs already assigned to obj via its tags M2M."""

return set(

obj.tags.filter(tag_group_id__in=PROTEIN_TAG_GROUP_IDS).values_list("id", flat=True)

)

def compute_cookbook_tag_ids(cookbook: Cookbook) -> set[int]:

items = (

CookbookItem.objects

.filter(section__cookbook=cookbook)

.select_related("pairing", "product")

.prefetch_related("pairing__tags", "product__tags")

)

tag_ids: set[int] = set()

for item in items:

obj = item.pairing if item.item_type == CookbookItemType.PAIRING else item.product

if obj is None:

continue

tag_ids |= allergen_tag_ids(obj)

tag_ids |= protein_tag_ids(obj)

return tag_ids

def update_tags(cookbook_id=None, dry_run=True):

qs = Cookbook.objects.all()

if cookbook_id is not None:

qs = qs.filter(id=cookbook_id)

for cookbook in qs:

computed = compute_cookbook_tag_ids(cookbook)

existing = set(cookbook.dietary_tags.values_list("id", flat=True))

to_add = computed - existing

print(

f"Cookbook {cookbook.id} {cookbook.slug!r}: existing={sorted(existing)} "

f"computed={sorted(computed)} adding={sorted(to_add)}"

)

if dry_run:

print("Dry run, no changes made")

continue

if not to_add:

print("No new tags to add, no changes made")

continue

with transaction.atomic():

CookbookDietaryTag.objects.bulk_create(

[CookbookDietaryTag(cookbook=cookbook, tag_id=tid) for tid in to_add],

ignore_conflicts=True,

)

print("done")

#SRC_END