restore composer.json, add mysqli extension
This commit is contained in:
22
core/apps/vendors/migrations/0007_remove_productvariantmodel_image_url_and_more.py
vendored
Normal file
22
core/apps/vendors/migrations/0007_remove_productvariantmodel_image_url_and_more.py
vendored
Normal file
@@ -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"),
|
||||
),
|
||||
]
|
||||
2
core/apps/vendors/models/product_variant.py
vendored
2
core/apps/vendors/models/product_variant.py
vendored
@@ -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):
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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}")
|
||||
|
||||
Reference in New Issue
Block a user