mirror of
https://github.com/narrrl/holy-flavor-rater.git
synced 2026-11-04 00:47:02 +01:00
refactor
This commit is contained in:
1 parent
5e3474dd59
commit
e4d59a13ba
119 files changed
+15490
-9975
No files matched your search
@@ -1,38 +1,43 @@
|
||||
import sqlite3
|
||||
import os
|
||||
import shutil
|
||||
import sqlite3
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Creates a safe, consistent backup of the SQLite database and manages rotation.'
|
||||
help = "Creates a safe, consistent backup of the SQLite database and manages rotation."
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument('--output', type=str, help='Specific output path for the backup file')
|
||||
parser.add_argument('--retention', type=int, default=7, help='Number of days to keep old backups')
|
||||
parser.add_argument('--full', action='store_true', help='Create a full archive including media')
|
||||
parser.add_argument("--output", type=str, help="Specific output path for the backup file")
|
||||
parser.add_argument(
|
||||
"--retention", type=int, default=7, help="Number of days to keep old backups"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--full", action="store_true", help="Create a full archive including media"
|
||||
)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
db_conf = settings.DATABASES['default']
|
||||
if db_conf['ENGINE'] != 'django.db.backends.sqlite3':
|
||||
db_conf = settings.DATABASES["default"]
|
||||
if db_conf["ENGINE"] != "django.db.backends.sqlite3":
|
||||
self.stdout.write(self.style.ERROR("This command only supports SQLite."))
|
||||
return
|
||||
|
||||
db_path = str(db_conf['NAME'])
|
||||
backup_dir = os.path.join(settings.BASE_DIR, 'backups')
|
||||
db_path = str(db_conf["NAME"])
|
||||
backup_dir = os.path.join(settings.BASE_DIR, "backups")
|
||||
os.makedirs(backup_dir, exist_ok=True)
|
||||
|
||||
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
||||
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
|
||||
# 1. Database Backup
|
||||
if options['output']:
|
||||
db_backup_path = options['output']
|
||||
if options["output"]:
|
||||
db_backup_path = options["output"]
|
||||
else:
|
||||
db_backup_path = os.path.join(backup_dir, f'db_backup_{timestamp}.sqlite3')
|
||||
db_backup_path = os.path.join(backup_dir, f"db_backup_{timestamp}.sqlite3")
|
||||
|
||||
self.stdout.write(f"Cloning database {db_path}...")
|
||||
|
||||
|
||||
try:
|
||||
src = sqlite3.connect(db_path)
|
||||
dst = sqlite3.connect(db_backup_path)
|
||||
@@ -46,12 +51,13 @@ class Command(BaseCommand):
|
||||
return
|
||||
|
||||
# 2. Full Archive (Optional)
|
||||
if options['full']:
|
||||
if options["full"]:
|
||||
import tarfile
|
||||
|
||||
archive_name = f"full_backup_{timestamp}.tar.gz"
|
||||
archive_path = os.path.join(backup_dir, archive_name)
|
||||
self.stdout.write(f"Creating full archive at {archive_path}...")
|
||||
|
||||
|
||||
try:
|
||||
with tarfile.open(archive_path, "w:gz") as tar:
|
||||
# Add database
|
||||
@@ -60,19 +66,19 @@ class Command(BaseCommand):
|
||||
media_dir = str(settings.MEDIA_ROOT)
|
||||
if os.path.exists(media_dir):
|
||||
tar.add(media_dir, arcname="media")
|
||||
|
||||
|
||||
# Remove the raw sqlite backup after bundling it into the archive
|
||||
if not options['output']:
|
||||
if not options["output"]:
|
||||
os.remove(db_backup_path)
|
||||
|
||||
self.stdout.write(self.style.SUCCESS(f"Full archive created successfully."))
|
||||
|
||||
self.stdout.write(self.style.SUCCESS("Full archive created successfully."))
|
||||
except Exception as e:
|
||||
self.stdout.write(self.style.ERROR(f"Archive creation failed: {e}"))
|
||||
|
||||
# 3. Cleanup Old Backups
|
||||
retention_days = options['retention']
|
||||
retention_days = options["retention"]
|
||||
cutoff = datetime.now() - timedelta(days=retention_days)
|
||||
|
||||
|
||||
self.stdout.write(f"Cleaning up backups older than {retention_days} days...")
|
||||
for filename in os.listdir(backup_dir):
|
||||
file_path = os.path.join(backup_dir, filename)
|
||||
@@ -81,6 +87,10 @@ class Command(BaseCommand):
|
||||
if file_time < cutoff:
|
||||
try:
|
||||
os.remove(file_path)
|
||||
self.stdout.write(self.style.SUCCESS(f" -> Deleted old backup: {filename}"))
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(f" -> Deleted old backup: {filename}")
|
||||
)
|
||||
except Exception as e:
|
||||
self.stdout.write(self.style.WARNING(f" -> Failed to delete {filename}: {e}"))
|
||||
self.stdout.write(
|
||||
self.style.WARNING(f" -> Failed to delete {filename}: {e}")
|
||||
)
|
||||
@@ -1,34 +1,39 @@
|
||||
from django.core.management.base import BaseCommand
|
||||
from api.models import Flavor, Rating
|
||||
from django.db.models import Count
|
||||
|
||||
from api.models import Flavor, Rating
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Identifies and merges duplicate flavors (same name and category)'
|
||||
help = "Identifies and merges duplicate flavors (same name and category)"
|
||||
|
||||
def handle(self, *args, **options):
|
||||
# Find duplicates based on name and category
|
||||
duplicates = Flavor.objects.values('name', 'category').annotate(
|
||||
name_count=Count('id')
|
||||
).filter(name_count__gt=1)
|
||||
duplicates = (
|
||||
Flavor.objects.values("name", "category")
|
||||
.annotate(name_count=Count("id"))
|
||||
.filter(name_count__gt=1)
|
||||
)
|
||||
|
||||
self.stdout.write(f"Found {duplicates.count()} sets of duplicates.")
|
||||
|
||||
for dup in duplicates:
|
||||
name = dup['name']
|
||||
category_id = dup['category']
|
||||
|
||||
name = dup["name"]
|
||||
category_id = dup["category"]
|
||||
|
||||
# Get all flavors in this set, ordered by having an external_id first, then by date
|
||||
flavor_set = Flavor.objects.filter(
|
||||
name=name,
|
||||
category_id=category_id
|
||||
).order_by('-external_id', 'created_at')
|
||||
|
||||
flavor_set = Flavor.objects.filter(name=name, category_id=category_id).order_by(
|
||||
"-external_id", "created_at"
|
||||
)
|
||||
|
||||
# Keep the first one, merge others into it
|
||||
keep = flavor_set[0]
|
||||
others = flavor_set[1:]
|
||||
|
||||
self.stdout.write(f"Merging {len(others)} duplicates into '{keep.name}' (ID: {keep.id}, Ext: {keep.external_id})")
|
||||
|
||||
|
||||
self.stdout.write(
|
||||
f"Merging {len(others)} duplicates into '{keep.name}' (ID: {keep.id}, Ext: {keep.external_id})"
|
||||
)
|
||||
|
||||
for other in others:
|
||||
# Re-assign ratings
|
||||
for rating in Rating.objects.filter(flavor=other):
|
||||
@@ -39,7 +44,7 @@ class Command(BaseCommand):
|
||||
# User already has a rating for the 'keep' flavor
|
||||
# Delete the duplicate rating instead
|
||||
rating.delete()
|
||||
|
||||
|
||||
# Delete the duplicate flavor
|
||||
other.delete()
|
||||
|
||||
|
||||
@@ -1,64 +1,75 @@
|
||||
from django.core.management.base import BaseCommand
|
||||
from api.models import Banner
|
||||
import os
|
||||
import json
|
||||
import os
|
||||
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
from api.models import Banner
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Seeds banner models and missing settings from JSON files in backend/banners/'
|
||||
help = "Seeds banner models and missing settings from JSON files in backend/banners/"
|
||||
|
||||
def handle(self, *args, **options):
|
||||
# Determine the banners directory relative to the project root
|
||||
project_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(__file__))))
|
||||
banners_dir = os.path.join(project_root, 'banners')
|
||||
|
||||
banners_dir = os.path.join(project_root, "banners")
|
||||
|
||||
if not os.path.exists(banners_dir):
|
||||
self.stdout.write(self.style.WARNING(f"Banners directory not found at {banners_dir}"))
|
||||
return
|
||||
|
||||
for filename in os.listdir(banners_dir):
|
||||
if filename.endswith('.json'):
|
||||
if filename.endswith(".json"):
|
||||
filepath = os.path.join(banners_dir, filename)
|
||||
try:
|
||||
with open(filepath, 'r') as f:
|
||||
with open(filepath) as f:
|
||||
data = json.load(f)
|
||||
|
||||
slug = data.get('slug')
|
||||
|
||||
slug = data.get("slug")
|
||||
if not slug:
|
||||
continue
|
||||
|
||||
banner, created = Banner.objects.get_or_create(
|
||||
slug=slug,
|
||||
defaults={
|
||||
'name': data.get('name', slug),
|
||||
'description': data.get('description', ''),
|
||||
'settings': data.get('settings', {})
|
||||
}
|
||||
"name": data.get("name", slug),
|
||||
"description": data.get("description", ""),
|
||||
"settings": data.get("settings", {}),
|
||||
},
|
||||
)
|
||||
|
||||
if not created:
|
||||
# Sync missing settings from JSON to DB without overwriting existing keys
|
||||
db_settings = banner.settings or {}
|
||||
json_settings = data.get('settings', {})
|
||||
json_schema = data.get('schema', [])
|
||||
|
||||
json_settings = data.get("settings", {})
|
||||
json_schema = data.get("schema", [])
|
||||
|
||||
updated = False
|
||||
# Sync missing settings
|
||||
for key, value in json_settings.items():
|
||||
if key not in db_settings:
|
||||
db_settings[key] = value
|
||||
updated = True
|
||||
|
||||
|
||||
# Always sync schema as it represents the UI structure
|
||||
if banner.schema != json_schema:
|
||||
banner.schema = json_schema
|
||||
updated = True
|
||||
|
||||
|
||||
if updated:
|
||||
banner.settings = db_settings
|
||||
banner.save()
|
||||
self.stdout.write(self.style.SUCCESS(f"Synced new settings for banner '{slug}' from {filename}"))
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(
|
||||
f"Synced new settings for banner '{slug}' from {filename}"
|
||||
)
|
||||
)
|
||||
else:
|
||||
self.stdout.write(self.style.SUCCESS(f"Created new banner '{slug}' from {filename}"))
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(f"Created new banner '{slug}' from {filename}")
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
self.stdout.write(self.style.ERROR(f"Failed to sync banner from {filename}: {e}"))
|
||||
self.stdout.write(
|
||||
self.style.ERROR(f"Failed to sync banner from {filename}: {e}")
|
||||
)
|
||||
@@ -1,41 +1,45 @@
|
||||
import json
|
||||
import os
|
||||
|
||||
import requests
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.core.files import File
|
||||
from django.conf import settings
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.utils.text import slugify
|
||||
|
||||
from api.models import Category, Flavor
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Adds legacy flavors from JSON files in the legacy/ folder'
|
||||
help = "Adds legacy flavors from JSON files in the legacy/ folder"
|
||||
|
||||
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'
|
||||
|
||||
ext = url.split(".")[-1].split("?")[0].lower()
|
||||
if ext not in ["jpg", "jpeg", "png", "webp", "gif"]:
|
||||
ext = "png"
|
||||
|
||||
filename = f"legacy_{safe_name}.{ext}"
|
||||
filepath = os.path.join(settings.MEDIA_ROOT, 'flavors', filename)
|
||||
|
||||
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 = 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:
|
||||
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}"))
|
||||
self.stdout.write(
|
||||
self.style.WARNING(f"Failed to download image for {flavor_name}: {e}")
|
||||
)
|
||||
return None
|
||||
|
||||
def handle(self, *args, **options):
|
||||
@@ -43,88 +47,103 @@ class Command(BaseCommand):
|
||||
# Instead, we update existing entries.
|
||||
|
||||
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],
|
||||
"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],
|
||||
}
|
||||
|
||||
legacy_dir = os.path.join(settings.BASE_DIR, 'legacy')
|
||||
legacy_dir = os.path.join(settings.BASE_DIR, "legacy")
|
||||
if not os.path.exists(legacy_dir):
|
||||
legacy_dir = os.path.join(settings.BASE_DIR, '..', 'legacy')
|
||||
|
||||
legacy_dir = os.path.join(settings.BASE_DIR, "..", "legacy")
|
||||
|
||||
if not os.path.exists(legacy_dir):
|
||||
self.stdout.write(self.style.ERROR(f"Legacy directory not found."))
|
||||
self.stdout.write(self.style.ERROR("Legacy directory not found."))
|
||||
return
|
||||
|
||||
json_files = [f for f in os.listdir(legacy_dir) if f.endswith('.json')]
|
||||
json_files = [f for f in os.listdir(legacy_dir) if f.endswith(".json")]
|
||||
processed_count = 0
|
||||
|
||||
for json_file in json_files:
|
||||
file_path = os.path.join(legacy_dir, json_file)
|
||||
file_lower = json_file.lower()
|
||||
|
||||
|
||||
# Determine category from filename
|
||||
if 'energy' in file_lower:
|
||||
file_cat = cat_map['energy']
|
||||
elif 'iced_tea' in file_lower or 'iced-tea' in file_lower:
|
||||
file_cat = cat_map['iced-tea']
|
||||
elif 'hydration' in file_lower:
|
||||
file_cat = cat_map['hydration']
|
||||
elif 'milkshake' in file_lower:
|
||||
file_cat = cat_map['milkshake']
|
||||
if "energy" in file_lower:
|
||||
file_cat = cat_map["energy"]
|
||||
elif "iced_tea" in file_lower or "iced-tea" in file_lower:
|
||||
file_cat = cat_map["iced-tea"]
|
||||
elif "hydration" in file_lower:
|
||||
file_cat = cat_map["hydration"]
|
||||
elif "milkshake" in file_lower:
|
||||
file_cat = cat_map["milkshake"]
|
||||
else:
|
||||
file_cat = None
|
||||
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
with open(file_path, encoding="utf-8") as f:
|
||||
data_list = json.load(f)
|
||||
|
||||
|
||||
for data in data_list:
|
||||
name = data.get('Name')
|
||||
desc = data.get('Beschreibung')
|
||||
taste = data.get('Geschmack')
|
||||
status_text = data.get('Status')
|
||||
image_url = data.get('Bild_URL')
|
||||
name = data.get("Name")
|
||||
desc = data.get("Beschreibung")
|
||||
taste = data.get("Geschmack")
|
||||
status_text = data.get("Status")
|
||||
image_url = data.get("Bild_URL")
|
||||
|
||||
full_desc = f"**Geschmack:** {taste}\n\n{desc}\n\n**Status:** {status_text}"
|
||||
|
||||
|
||||
# Determine final category
|
||||
cat = file_cat
|
||||
if not cat:
|
||||
# Fallback to name/desc detection if filename was ambiguous
|
||||
if "hydration" in name.lower() or "hydration" in desc.lower():
|
||||
cat = cat_map['hydration']
|
||||
cat = cat_map["hydration"]
|
||||
elif "iced tea" in name.lower() or "eistee" in name.lower():
|
||||
cat = cat_map['iced-tea']
|
||||
cat = cat_map["iced-tea"]
|
||||
elif "milkshake" in name.lower():
|
||||
cat = cat_map['milkshake']
|
||||
elif any(k in name.lower() for k in ['bundle', 'set', 'box', 'probe', 'sample', 'taster', 'shaker', 'starter']):
|
||||
cat = cat_map['packs']
|
||||
cat = cat_map["milkshake"]
|
||||
elif any(
|
||||
k in name.lower()
|
||||
for k in [
|
||||
"bundle",
|
||||
"set",
|
||||
"box",
|
||||
"probe",
|
||||
"sample",
|
||||
"taster",
|
||||
"shaker",
|
||||
"starter",
|
||||
]
|
||||
):
|
||||
cat = cat_map["packs"]
|
||||
else:
|
||||
cat = cat_map['energy'] # Ultimate fallback
|
||||
cat = cat_map["energy"] # Ultimate fallback
|
||||
|
||||
# Try to find existing flavor by name and category regardless of legacy status
|
||||
flavor = Flavor.objects.filter(name__iexact=name, category=cat).first()
|
||||
|
||||
|
||||
# If it exists and is an active Shopify product, skip legacy processing
|
||||
if flavor and not flavor.is_legacy:
|
||||
self.stdout.write(f"Skipping active flavor: {name}")
|
||||
continue
|
||||
|
||||
|
||||
if not flavor:
|
||||
flavor = Flavor(
|
||||
name=name,
|
||||
category=cat,
|
||||
is_legacy=True,
|
||||
external_id=None
|
||||
)
|
||||
flavor = Flavor(name=name, category=cat, is_legacy=True, external_id=None)
|
||||
self.stdout.write(f"Adding new legacy flavor: {name}")
|
||||
else:
|
||||
self.stdout.write(f"Updating legacy flavor: {name}")
|
||||
|
||||
flavor.description = full_desc
|
||||
flavor.is_available = False # Legacy is always unavailable via API
|
||||
flavor.is_available = False # Legacy is always unavailable via API
|
||||
|
||||
if image_url:
|
||||
# Check if local file exists
|
||||
@@ -140,9 +159,11 @@ class Command(BaseCommand):
|
||||
if rel_path:
|
||||
flavor.image = rel_path
|
||||
self.stdout.write(f" -> Updated image for: {name}")
|
||||
|
||||
|
||||
flavor.image_url = image_url
|
||||
flavor.save()
|
||||
processed_count += 1
|
||||
|
||||
self.stdout.write(self.style.SUCCESS(f"Finished! Processed {processed_count} legacy flavors."))
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(f"Finished! Processed {processed_count} legacy flavors.")
|
||||
)
|
||||
@@ -1,21 +1,22 @@
|
||||
import json
|
||||
import requests
|
||||
import re
|
||||
import os
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.core.files import File
|
||||
import re
|
||||
|
||||
import requests
|
||||
from django.conf import settings
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.utils.text import slugify
|
||||
|
||||
from api.models import Category, Flavor
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Syncs flavors from Holy Energy Shopify API'
|
||||
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)
|
||||
cleanr = re.compile("<.*?>")
|
||||
cleantext = re.sub(cleanr, "", raw_html)
|
||||
return cleantext.strip()
|
||||
|
||||
def download_image(self, url, flavor_name):
|
||||
@@ -23,50 +24,60 @@ class Command(BaseCommand):
|
||||
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'
|
||||
|
||||
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)
|
||||
|
||||
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 = 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:
|
||||
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}"))
|
||||
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 = 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', [])
|
||||
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],
|
||||
"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
|
||||
@@ -74,82 +85,97 @@ class Command(BaseCommand):
|
||||
synced_external_ids = []
|
||||
|
||||
for p in products:
|
||||
title = p.get('title', '').strip()
|
||||
tags = p.get('tags', [])
|
||||
title = p.get("title", "").strip()
|
||||
tags = p.get("tags", [])
|
||||
if isinstance(tags, str):
|
||||
tags = [t.strip() for t in tags.split(',')]
|
||||
|
||||
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']
|
||||
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:
|
||||
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']
|
||||
|
||||
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']
|
||||
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')
|
||||
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()
|
||||
|
||||
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)
|
||||
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']
|
||||
|
||||
flavor.external_id = p["id"]
|
||||
|
||||
# Reactivate the flavor if it was previously marked as legacy
|
||||
if flavor.is_legacy:
|
||||
flavor.is_legacy = False
|
||||
|
||||
|
||||
# Ensure all synced fields are accurately reflected
|
||||
if flavor.name != title and not Flavor.objects.filter(name=title, category=category).exclude(pk=flavor.pk).exists():
|
||||
if (
|
||||
flavor.name != title
|
||||
and not Flavor.objects.filter(name=title, category=category)
|
||||
.exclude(pk=flavor.pk)
|
||||
.exists()
|
||||
):
|
||||
flavor.name = title
|
||||
|
||||
|
||||
flavor.description = description
|
||||
flavor.shop_url = shop_url
|
||||
flavor.is_available = is_available
|
||||
|
||||
|
||||
if image_url:
|
||||
# Check if local file exists
|
||||
file_exists = False
|
||||
@@ -165,11 +191,21 @@ class Command(BaseCommand):
|
||||
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}"))
|
||||
|
||||
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'])
|
||||
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}"))
|
||||
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}"
|
||||
)
|
||||
)
|
||||
@@ -1,81 +0,0 @@
|
||||
import time
|
||||
import sys
|
||||
from datetime import datetime, timedelta
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.core.management import call_command
|
||||
from django.utils import timezone
|
||||
from django.db import models
|
||||
from api.models import Job
|
||||
from io import StringIO
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Background worker to process scheduled and on-demand jobs.'
|
||||
|
||||
def handle(self, *args, **options):
|
||||
self.stdout.write(self.style.SUCCESS("Starting Holy Archive Worker..."))
|
||||
|
||||
# Ensure all core jobs exist in the DB
|
||||
for job_type, label in Job.JOB_TYPES:
|
||||
Job.objects.get_or_create(name=job_type)
|
||||
|
||||
while True:
|
||||
# 1. Find jobs that are either explicitly set to 'pending' (manual trigger)
|
||||
# or those where next_run has passed.
|
||||
now = timezone.now()
|
||||
jobs_to_run = Job.objects.filter(
|
||||
~models.Q(status='running')
|
||||
).filter(
|
||||
# Run if pending OR (next_run is set AND has passed)
|
||||
models.Q(status='pending') |
|
||||
models.Q(next_run__lte=now, interval_hours__gt=0)
|
||||
)
|
||||
|
||||
for job in jobs_to_run:
|
||||
self.run_job(job)
|
||||
|
||||
time.sleep(30) # Check every 30 seconds
|
||||
|
||||
def run_job(self, job):
|
||||
self.stdout.write(f"Executing Job: {job.get_name_display()}...")
|
||||
job.status = 'running'
|
||||
job.last_run = timezone.now()
|
||||
job.last_output = "" # Reset output for new run
|
||||
job.save()
|
||||
|
||||
# Capture management command output
|
||||
class LiveStream(StringIO):
|
||||
def write(self, s):
|
||||
super().write(s)
|
||||
# Periodically update the DB with current output
|
||||
job.last_output = self.getvalue()
|
||||
job.save(update_fields=['last_output'])
|
||||
|
||||
out = LiveStream()
|
||||
error = ""
|
||||
|
||||
try:
|
||||
if job.name == 'sync_flavors':
|
||||
call_command('sync_flavors', stdout=out)
|
||||
elif job.name == 'cleanup_duplicates':
|
||||
call_command('cleanup_duplicates', stdout=out)
|
||||
elif job.name == 'backup_db':
|
||||
call_command('backup_db', '--full', stdout=out)
|
||||
elif job.name == 'seed_legacy':
|
||||
call_command('seed_legacy_flavors', stdout=out)
|
||||
|
||||
job.status = 'completed'
|
||||
except Exception as e:
|
||||
job.status = 'failed'
|
||||
error = str(e)
|
||||
self.stdout.write(self.style.ERROR(f"Job failed: {error}"))
|
||||
|
||||
# Update scheduling for next run if interval is set
|
||||
if job.interval_hours > 0:
|
||||
job.next_run = timezone.now() + timedelta(hours=job.interval_hours)
|
||||
else:
|
||||
job.next_run = None
|
||||
|
||||
job.last_output = out.getvalue()
|
||||
job.error_message = error
|
||||
job.save()
|
||||
self.stdout.write(self.style.SUCCESS(f"Job finished: {job.status}"))
|
||||
Reference in new issue
Block a user