From 524c5cc69ce96bfd70007d3b62b02a0713b32f2e Mon Sep 17 00:00:00 2001 From: husanjon Date: Mon, 13 Apr 2026 17:31:07 +0500 Subject: [PATCH] restore composer.json, add mysqli extension --- ..._productvariantmodel_image_url_and_more.py | 22 ++++++++ core/apps/vendors/models/product_variant.py | 2 +- .../vendor_product/VendorProduct.py | 14 ++++- sync_firebase_products.py | 56 +++++++++++++++---- 4 files changed, 82 insertions(+), 12 deletions(-) create mode 100644 core/apps/vendors/migrations/0007_remove_productvariantmodel_image_url_and_more.py diff --git a/core/apps/vendors/migrations/0007_remove_productvariantmodel_image_url_and_more.py b/core/apps/vendors/migrations/0007_remove_productvariantmodel_image_url_and_more.py new file mode 100644 index 0000000..c90175b --- /dev/null +++ b/core/apps/vendors/migrations/0007_remove_productvariantmodel_image_url_and_more.py @@ -0,0 +1,22 @@ +# Generated by Django 6.0.4 on 2026-04-13 12:28 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("vendors", "0006_productattributemodel_productvariantmodel"), + ] + + operations = [ + migrations.RemoveField( + model_name="productvariantmodel", + name="image_url", + ), + migrations.AddField( + model_name="productvariantmodel", + name="image", + field=models.ImageField(blank=True, null=True, upload_to="variants/", verbose_name="image"), + ), + ] diff --git a/core/apps/vendors/models/product_variant.py b/core/apps/vendors/models/product_variant.py index 20566f8..e7c6567 100644 --- a/core/apps/vendors/models/product_variant.py +++ b/core/apps/vendors/models/product_variant.py @@ -14,7 +14,7 @@ class ProductVariantModel(AbstractBaseModel): price = models.DecimalField(verbose_name=_("price"), max_digits=12, decimal_places=2, default=0) sku = models.CharField(verbose_name=_("SKU"), max_length=255, null=True, blank=True) quantity = models.IntegerField(verbose_name=_("quantity"), default=-1) - image_url = models.URLField(verbose_name=_("image url"), max_length=1000, null=True, blank=True) + image = models.ImageField(verbose_name=_("image"), upload_to="variants/", null=True, blank=True) attribute_data = models.JSONField(verbose_name=_("attribute data"), null=True, blank=True) def __str__(self): diff --git a/core/apps/vendors/serializers/vendor_product/VendorProduct.py b/core/apps/vendors/serializers/vendor_product/VendorProduct.py index 6f5067f..6303dfa 100644 --- a/core/apps/vendors/serializers/vendor_product/VendorProduct.py +++ b/core/apps/vendors/serializers/vendor_product/VendorProduct.py @@ -22,12 +22,24 @@ class ProductVariantSerializer(serializers.ModelSerializer): "price", "sku", "quantity", - "image_url", + "image", "attribute_data", ] def to_representation(self, instance): ret = super().to_representation(instance) + + # Absolute URL for variant image + request = self.context.get("request") + image = ret.get("image") + if image and isinstance(image, str) and not image.startswith("http"): + if not image.startswith("/resources/"): + image = f"/resources/media/{image.lstrip('/')}" + if request: + ret["image"] = request.build_absolute_uri(image) + else: + ret["image"] = image + attr_data = ret.get("attribute_data") if attr_data and isinstance(attr_data, list): diff --git a/sync_firebase_products.py b/sync_firebase_products.py index 08aeb04..89ebc57 100644 --- a/sync_firebase_products.py +++ b/sync_firebase_products.py @@ -24,6 +24,8 @@ import firebase_admin from firebase_admin import credentials, firestore from decimal import Decimal, InvalidOperation from datetime import datetime, timezone +import requests +from django.core.files.base import ContentFile # ── Django sozlash ─────────────────────────────────────────────────────────── BASE_DIR = os.path.dirname(os.path.abspath(__file__)) @@ -131,18 +133,26 @@ def build_product_data(doc_id: str, data: dict) -> dict: ) -def print_progress(current, total, created, updated, skipped, errors): - percent = int(current / total * 100) if total else 0 - bar_filled = percent // 5 - bar = "█" * bar_filled + "░" * (20 - bar_filled) - line = ( - f"\r [{bar}] {percent:3d}% " - f"{current}/{total} | " - f"✓ {created} ↑ {updated} — {skipped} ✗ {errors}" - ) print(line, end="", flush=True) +def download_and_save_image(url: str, model_instance, field_name: str, filename: str): + """Rasmni yuklab oladi va modelning ko'rsatilgan maydoniga saqlaydi.""" + if not url or not url.startswith("http"): + return False + try: + response = requests.get(url, timeout=10) + if response.status_code == 200: + content = ContentFile(response.content) + field = getattr(model_instance, field_name) + # S3'da duplicatelarning oldini olish uchun save(name, content, save=True) + field.save(filename, content, save=True) + return True + except Exception as e: + print(f"\n [IMG XATO] {url}: {e}") + return False + + def sync_attributes(): print("Firebase vendor_attributes kolleksiyasi o'qilmoqda…") docs = db.collection("vendor_attributes").stream() @@ -224,6 +234,21 @@ def sync_products(dry_run: bool = False, update: bool = False, after: datetime | ) created += 1 + # ── Rasmni yuklab olish (Asosiy mahsulot uchun) ──────────────────── + + if not dry_run and product_obj: + # Agar asosiy rasm bo'lmasa, uni yuklab olamiz + if not product_obj.image and product_data.get("photos_json"): + first_photo = product_data["photos_json"][0] + if isinstance(first_photo, str): + ext = first_photo.split(".")[-1].split("?")[0] or "jpg" + download_and_save_image( + first_photo, + product_obj, + "image", + f"product_{doc_id}.{ext}" + ) + # ── Variantlarni qayta ishlash ──────────────────────────────────── item_attr = data.get("item_attribute") @@ -244,10 +269,21 @@ def sync_products(dry_run: bool = False, update: bool = False, after: datetime | "price": to_decimal(v_data.get("variant_price", 0)), "sku": v_data.get("variant_sku"), "quantity": to_int(v_data.get("variant_quantity", -1)), - "image_url": v_data.get("variant_image"), "attribute_data": item_attr.get("attributes") } ) + + # Variant rasm yuklash + v_obj = ProductVariantModel.objects.filter(firestore_id=v_id).first() + if v_obj and not v_obj.image and v_data.get("variant_image"): + v_img_url = v_data["variant_image"] + ext = v_img_url.split(".")[-1].split("?")[0] or "jpg" + download_and_save_image( + v_img_url, + v_obj, + "image", + f"variant_{v_id}.{ext}" + ) except Exception as e: print(f"\n [XATO] {doc_id}: {e}")