main image selections

This commit is contained in:
nvrl committed 2026-04-26 22:38:07 +02:00
1 parent caf3830d72
commit 466d0a0f9f
7 files changed
+153 -80

No files matched your search

+29 -1
View File
@@ -1,3 +1,4 @@
from django import forms
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
@@ -9,8 +10,35 @@ class CustomUserAdmin(UserAdmin):
add_fieldsets = UserAdmin.add_fieldsets + ((None, {"fields": ("theme",)}),)
class FlavorAdminForm(forms.ModelForm):
class Meta:
model = Flavor
exclude: list[str] = []
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
instance = kwargs.get("instance")
paths = list(instance.local_image_paths or []) if instance else []
if paths:
choices = [("", "— first available —")] + [(p, p.split("/")[-1]) for p in paths]
self.fields["main_image_path"] = forms.ChoiceField(
choices=choices,
required=False,
label="Main image",
help_text="Pick which downloaded image is used as the primary.",
)
class FlavorAdmin(admin.ModelAdmin):
form = FlavorAdminForm
list_display = ("name", "category", "is_available", "is_legacy", "external_id")
list_filter = ("category", "is_available", "is_legacy")
search_fields = ("name",)
readonly_fields = ("local_image_paths",)
admin.site.register(User, CustomUserAdmin)
admin.site.register(Category)
admin.site.register(Flavor)
admin.site.register(Flavor, FlavorAdmin)
admin.site.register(Rating)
admin.site.register(Banner)
@@ -1,5 +1,7 @@
import hashlib
import json
import os
from urllib.parse import urlparse
import requests
from django.conf import settings
@@ -13,29 +15,26 @@ class Command(BaseCommand):
help = "Adds legacy flavors from JSON files in the legacy/ folder"
def download_image(self, url, flavor_name):
"""Download legacy image into flavors/legacy_<slug>/00_<hash>.<ext>. Returns rel path."""
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"]:
path = urlparse(url).path
ext = path.rsplit(".", 1)[-1].lower() if "." in path else ""
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)
os.makedirs(os.path.dirname(filepath), exist_ok=True)
url_hash = hashlib.sha1(path.encode("utf-8")).hexdigest()[:10]
rel_path = os.path.join("flavors", f"legacy_{safe_name}", f"00_{url_hash}.{ext}")
abs_path = os.path.join(settings.MEDIA_ROOT, rel_path)
if os.path.exists(abs_path) and os.path.getsize(abs_path) > 0:
return rel_path
os.makedirs(os.path.dirname(abs_path), 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:
with open(abs_path, "wb") as f:
f.write(response.content)
return f"flavors/{filename}"
return rel_path
except Exception as e:
self.stdout.write(
self.style.WARNING(f"Failed to download image for {flavor_name}: {e}")
@@ -146,21 +145,18 @@ class Command(BaseCommand):
flavor.is_available = False # Legacy is always unavailable via API
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 if image_url changed
if not file_exists or flavor.image_url != image_url:
rel_path = self.download_image(image_url, name)
if rel_path:
flavor.image = rel_path
self.stdout.write(f" -> Updated image for: {name}")
rel_path = self.download_image(image_url, name)
if rel_path:
flavor.local_image_paths = [rel_path]
if (
flavor.main_image_path
and flavor.main_image_path not in flavor.local_image_paths
):
flavor.main_image_path = None
self.stdout.write(f" -> Image for: {name}")
flavor.image_url = image_url
flavor.image_urls = [image_url] if image_url else []
flavor.save()
processed_count += 1
+46 -46
View File
@@ -1,10 +1,11 @@
import hashlib
import os
import re
from urllib.parse import urlparse
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
@@ -19,36 +20,48 @@ class Command(BaseCommand):
cleantext = re.sub(cleanr, "", raw_html)
return cleantext.strip()
def download_image(self, url, flavor_name):
def download_image_to_path(self, url, rel_path):
"""Download `url` to MEDIA_ROOT/rel_path. Skip if file already exists. Returns rel_path or None."""
if not url:
return None
abs_path = os.path.join(settings.MEDIA_ROOT, rel_path)
if os.path.exists(abs_path) and os.path.getsize(abs_path) > 0:
return rel_path
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)
os.makedirs(os.path.dirname(abs_path), 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:
with open(abs_path, "wb") as f:
f.write(response.content)
return f"flavors/{filename}"
return rel_path
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 {url}: {e}"))
return None
@staticmethod
def _ext_for(url):
path = urlparse(url).path
ext = path.rsplit(".", 1)[-1].lower() if "." in path else ""
return ext if ext in ("jpg", "jpeg", "png", "webp", "gif") else "png"
@staticmethod
def _hash_for(url):
path = urlparse(url).path
return hashlib.sha1(path.encode("utf-8")).hexdigest()[:10]
def download_gallery(self, image_urls, dir_slug):
"""Download every URL into flavors/<dir_slug>/<i>_<hash>.<ext>. Returns list of rel paths."""
paths = []
for i, url in enumerate(image_urls):
if not url:
continue
filename = f"{i:02d}_{self._hash_for(url)}.{self._ext_for(url)}"
rel = os.path.join("flavors", dir_slug, filename)
saved = self.download_image_to_path(url, rel)
if saved:
paths.append(saved)
return paths
def handle(self, *args, **options):
url = "https://weareholy.com/products.json?limit=250"
self.stdout.write(f"Fetching from {url}...")
@@ -143,12 +156,10 @@ class Command(BaseCommand):
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:
@@ -156,15 +167,11 @@ class Command(BaseCommand):
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
# Ensure all synced fields are accurately reflected
if (
flavor.name != title
and not Flavor.objects.filter(name=title, category=category)
@@ -177,24 +184,17 @@ class Command(BaseCommand):
flavor.shop_url = shop_url
flavor.is_available = is_available
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}")
)
# Download whole gallery into flavors/<external_id>/
dir_slug = str(p["id"])
local_paths = self.download_gallery(image_urls_list, dir_slug)
if local_paths:
flavor.local_image_paths = local_paths
# Drop main_image_path if it points to a file no longer in the gallery
if flavor.main_image_path and flavor.main_image_path not in local_paths:
flavor.main_image_path = None
elif not flavor.local_image_paths:
# No new paths and nothing previously stored — leave both empty
flavor.main_image_path = None
flavor.image_url = image_url
flavor.image_urls = image_urls_list
@@ -0,0 +1,23 @@
# Generated by Django 6.0.4 on 2026-04-26 18:45
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('api', '0031_alter_job_name'),
]
operations = [
migrations.AddField(
model_name='flavor',
name='local_image_paths',
field=models.JSONField(blank=True, default=list),
),
migrations.AddField(
model_name='flavor',
name='main_image_path',
field=models.CharField(blank=True, max_length=500, null=True),
),
]
+2
View File
@@ -21,6 +21,8 @@ class Flavor(models.Model):
image_url = models.URLField(max_length=1000, blank=True, null=True)
image_urls = models.JSONField(blank=True, default=list)
image = models.ImageField(upload_to="flavors/", blank=True, null=True)
local_image_paths = models.JSONField(blank=True, default=list)
main_image_path = models.CharField(max_length=500, blank=True, null=True)
shop_url = models.URLField(max_length=500, blank=True, null=True)
is_available = models.BooleanField(default=True)
is_legacy = models.BooleanField(default=False)
+13
View File
@@ -1,5 +1,7 @@
from typing import Any
from django.conf import settings
def absolute_image_url(serializer: Any, image_field: Any) -> str | None:
if not image_field:
@@ -8,3 +10,14 @@ def absolute_image_url(serializer: Any, image_field: Any) -> str | None:
if request:
return request.build_absolute_uri(image_field.url)
return image_field.url
def absolute_media_url(serializer: Any, rel_path: str | None) -> str | None:
if not rel_path:
return None
media_url = settings.MEDIA_URL.rstrip("/") + "/"
url = media_url + rel_path.lstrip("/")
request = serializer.context.get("request")
if request:
return request.build_absolute_uri(url)
return url
+16 -5
View File
@@ -2,7 +2,7 @@ from django.db.models import Count
from rest_framework import serializers
from api.models import Category, Flavor, Rating
from api.serializers._helpers import absolute_image_url
from api.serializers._helpers import absolute_image_url, absolute_media_url
from api.serializers.rating import RatingSerializer
@@ -47,18 +47,29 @@ class FlavorSerializer(serializers.ModelSerializer):
extra_kwargs = {"image": {"write_only": True}}
def get_image_url(self, obj: Flavor) -> str | None:
if obj.main_image_path:
url = absolute_media_url(self, obj.main_image_path)
if url:
return url
if obj.local_image_paths:
url = absolute_media_url(self, obj.local_image_paths[0])
if url:
return url
if obj.image:
return absolute_image_url(self, obj.image)
return obj.image_url
def get_image_urls(self, obj: Flavor) -> list[str]:
urls: list[str] = []
if obj.local_image_paths:
urls = [absolute_media_url(self, p) for p in obj.local_image_paths]
return [u for u in urls if u]
legacy_urls: list[str] = []
if obj.image:
absolute = absolute_image_url(self, obj.image)
if absolute:
urls.append(absolute)
urls.extend(obj.image_urls or [])
return urls
legacy_urls.append(absolute)
legacy_urls.extend(obj.image_urls or [])
return legacy_urls
def get_rating_distribution(self, obj: Flavor) -> dict[str, int]:
counts = dict.fromkeys((str(i) for i in range(1, 11)), 0)