import json import requests import re import os from django.core.management.base import BaseCommand from django.core.files import File from django.conf import settings from django.utils.text import slugify from api.models import Category, Flavor class Command(BaseCommand): help = 'Syncs flavors from Holy Energy Shopify API' def clean_html(self, raw_html): if not raw_html: return "" cleanr = re.compile('<.*?>') cleantext = re.sub(cleanr, '', raw_html) return cleantext.strip() def download_image(self, url, flavor_name): if not url: return None try: safe_name = slugify(flavor_name) ext = url.split('.')[-1].split('?')[0].lower() if ext not in ['jpg', 'jpeg', 'png', 'webp', 'gif']: ext = 'png' filename = f"{safe_name}.{ext}" filepath = os.path.join(settings.MEDIA_ROOT, 'flavors', filename) os.makedirs(os.path.dirname(filepath), exist_ok=True) response = requests.get(url, headers={'User-Agent': 'Mozilla/5.0'}, timeout=10) response.raise_for_status() if os.path.exists(filepath): os.remove(filepath) with open(filepath, 'wb') as f: f.write(response.content) return f"flavors/{filename}" except Exception as e: self.stdout.write(self.style.WARNING(f"Failed to download image for {flavor_name}: {e}")) return None def handle(self, *args, **options): url = "https://weareholy.com/products.json?limit=250" self.stdout.write(f"Fetching from {url}...") try: response = requests.get(url, headers={'User-Agent': 'Mozilla/5.0'}, timeout=15) response.raise_for_status() data = response.json() except Exception as e: self.stdout.write(self.style.ERROR(f"Failed to fetch data: {e}")) return products = data.get('products', []) self.stdout.write(f"Found {len(products)} products.") cat_map = { 'energy': Category.objects.get_or_create(name='Energy', defaults={'slug': 'energy'})[0], 'hydration': Category.objects.get_or_create(name='Hydration', defaults={'slug': 'hydration'})[0], 'iced-tea': Category.objects.get_or_create(name='Iced Tea', defaults={'slug': 'iced-tea'})[0], 'milkshake': Category.objects.get_or_create(name='Milkshake', defaults={'slug': 'milkshake'})[0], 'packs': Category.objects.get_or_create(name='Packs and other', defaults={'slug': 'packs-and-other'})[0], } created_count = 0 updated_count = 0 synced_external_ids = [] for p in products: title = p.get('title', '').strip() tags = p.get('tags', []) if isinstance(tags, str): tags = [t.strip() for t in tags.split(',')] tags_lower = [t.lower() for t in tags] title_lower = title.lower() is_pack = False pack_keywords = ['bundle', 'set', 'box', 'probe', 'sample', 'taster', 'starter', 'collection', 'probier'] if any(k in title_lower for k in pack_keywords): is_pack = True if 'shaker' in title_lower or 'merch' in tags_lower: is_pack = True category = None if is_pack: category = cat_map['packs'] elif 'holy energy' in tags_lower or 'energy' in tags_lower: category = cat_map['energy'] elif 'holy hydration' in tags_lower or 'hydration' in tags_lower: category = cat_map['hydration'] elif 'holy iced tea' in tags_lower or 'iced tea' in tags_lower: category = cat_map['iced-tea'] elif 'milkshake' in tags_lower: category = cat_map['milkshake'] if not category: if 'shaker' in tags_lower: category = cat_map['packs'] else: continue variants = p.get('variants', []) is_available = any(v.get('available') for v in variants) images = p.get('images', []) image_url = images[0].get('src') if images else None description = self.clean_html(p.get('body_html', '')) handle = p.get('handle') shop_url = f"https://weareholy.com/products/{handle}" if handle else None # Find or create - be very specific to avoid duplicates # 1. Try external_id flavor = Flavor.objects.filter(external_id=p['id']).first() if not flavor: # 2. Try exact name match in category # REMOVED: is_legacy=False flavor = Flavor.objects.filter(name=title, category=category).first() if not flavor: # 3. Try case-insensitive name match in category (safety for whitespace/case) # REMOVED: is_legacy=False flavor = Flavor.objects.filter(name__iexact=title, category=category).first() if not flavor: flavor = Flavor(external_id=p['id'], name=title, category=category, is_legacy=False) created_count += 1 else: updated_count += 1 # Ensure external_id is updated if it was missing if not flavor.external_id: flavor.external_id = p['id'] # Reactivate the flavor if it was previously marked as legacy if flavor.is_legacy: flavor.is_legacy = False if image_url: # Check if local file exists file_exists = False if flavor.image: abs_path = os.path.join(settings.MEDIA_ROOT, flavor.image.name) if os.path.exists(abs_path): file_exists = True # Download if missing or URL changed if not file_exists or flavor.image_url != image_url: rel_path = self.download_image(image_url, title) if rel_path: flavor.image = rel_path self.stdout.write(f"Downloaded image for: {title}") else: self.stdout.write(self.style.WARNING(f"Failed to ensure image for: {title}")) flavor.image_url = image_url flavor.save() synced_external_ids.append(p['id']) discontinued_count = Flavor.objects.filter(external_id__isnull=False).exclude(external_id__in=synced_external_ids).update(is_available=False) self.stdout.write(self.style.SUCCESS(f"Finished! Created: {created_count}, Updated: {updated_count}, Marked Unavailable: {discontinued_count}"))