categorylanri import qilish qoshildi

This commit is contained in:
Husanjonazamov
2026-03-26 14:06:37 +05:00
parent 00f9e6be54
commit bcea5f4a70
13 changed files with 319 additions and 56 deletions

View File

@@ -18,43 +18,25 @@ LOGGING = {
"disable_existing_loggers": False, "disable_existing_loggers": False,
"formatters": { "formatters": {
"verbose": { "verbose": {
"format": "%(asctime)s %(name)s %(levelname)s %(pathname)s:%(lineno)d - %(message)s", "format": "%(asctime)s %(name)s %(levelname)s - %(message)s",
},
},
"filters": {
"exclude_errors": {
"()": ExcludeErrorsFilter,
}, },
}, },
"handlers": { "handlers": {
"daily_rotating_file": { "console": {
"level": "INFO", "level": "DEBUG",
"class": "logging.handlers.TimedRotatingFileHandler", "class": "logging.StreamHandler",
"filename": LOG_DIR / "django.log",
"when": "midnight",
"backupCount": 30,
"formatter": "verbose",
"filters": ["exclude_errors"],
},
"error_file": {
"level": "ERROR",
"class": "logging.handlers.TimedRotatingFileHandler",
"filename": LOG_DIR / "django_error.log",
"when": "midnight",
"backupCount": 30,
"formatter": "verbose", "formatter": "verbose",
}, },
}, },
"root": {
"handlers": ["console"],
"level": "INFO",
},
"loggers": { "loggers": {
"django": { "django": {
"handlers": ["daily_rotating_file", "error_file"], "handlers": ["console"],
"level": "INFO", "level": "INFO",
"propagate": True, "propagate": False,
},
"root": {
"handlers": ["daily_rotating_file", "error_file"],
"level": "INFO",
"propagate": True,
}, },
}, },
} }

View File

View File

@@ -0,0 +1,60 @@
import requests
from django.core.management.base import BaseCommand
from django.core.files.base import ContentFile
from core.apps.vendors.models import VendorproductModel, CategoryModel, VendorModel
import os
class Command(BaseCommand):
help = 'Syncs images from photo_url/photos_json to ImageField if they are empty'
def handle(self, *args, **options):
self.sync_categories()
self.sync_vendors()
self.sync_products()
def download_image(self, url):
if not url or not url.startswith('http'):
return None
try:
response = requests.get(url, timeout=10)
if response.status_code == 200:
name = os.path.basename(url.split('?')[0])
if not name:
name = "image.png"
return ContentFile(response.content, name=name)
except Exception as e:
self.stdout.write(self.style.ERROR(f"Error downloading {url}: {e}"))
return None
def sync_categories(self):
self.stdout.write("Syncing Category images...")
for obj in CategoryModel.objects.filter(photo__isnull=True).exclude(photo_url__isnull=True):
if not obj.photo_url.startswith('http'): continue
file_content = self.download_image(obj.photo_url)
if file_content:
obj.photo.save(file_content.name, file_content, save=True)
self.stdout.write(self.style.SUCCESS(f"Saved photo for category: {obj.title}"))
def sync_vendors(self):
self.stdout.write("Syncing Vendor images...")
for obj in VendorModel.objects.filter(photo__isnull=True).exclude(photo_url__isnull=True):
if not obj.photo_url.startswith('http'): continue
file_content = self.download_image(obj.photo_url)
if file_content:
obj.photo.save(file_content.name, file_content, save=True)
self.stdout.write(self.style.SUCCESS(f"Saved photo for vendor: {obj.title}"))
def sync_products(self):
self.stdout.write("Syncing Product images...")
for obj in VendorproductModel.objects.filter(image__isnull=True).exclude(photos_json__isnull=True):
if not isinstance(obj.photos_json, list) or len(obj.photos_json) == 0:
continue
url = obj.photos_json[0]
if not isinstance(url, str) or not url.startswith('http'):
continue
file_content = self.download_image(url)
if file_content:
obj.image.save(file_content.name, file_content, save=True)
self.stdout.write(self.style.SUCCESS(f"Saved image for product: {obj.name}"))

View File

@@ -8,6 +8,13 @@ from core.apps.vendors.models import CategoryModel
class CategoryAdmin(ModelAdmin): class CategoryAdmin(ModelAdmin):
list_display = ( list_display = (
"id", "id",
"__str__", "title",
"section",
"photo",
"photo_url",
"is_publish",
"order",
) )
search_fields = ("title", "firestore_id")
list_filter = ("section", "is_publish")

View File

@@ -8,13 +8,26 @@ from core.apps.vendors.models import ProductimageModel, VendorproductModel
class VendorproductAdmin(ModelAdmin): class VendorproductAdmin(ModelAdmin):
list_display = ( list_display = (
"id", "id",
"__str__", "name",
"vendor",
"category",
"section",
"price",
"quantity",
"is_publish",
"image",
) )
search_fields = ("name", "firestore_id", "vendor")
list_filter = ("is_publish", "category", "section")
autocomplete_fields = ("category", "section")
@admin.register(ProductimageModel) @admin.register(ProductimageModel)
class ProductimageAdmin(ModelAdmin): class ProductimageAdmin(ModelAdmin):
list_display = ( list_display = (
"id", "id",
"__str__", "product",
"image",
"order",
) )
list_filter = ("product",)

View File

@@ -19,23 +19,58 @@ class BaseCategorySerializer(serializers.ModelSerializer):
"is_publish", "is_publish",
"order", "order",
] ]
def to_representation(self, instance):
ret = super().to_representation(instance)
request = self.context.get("request")
photo_url = ret.get("photo_url")
if photo_url and isinstance(photo_url, str):
if not photo_url.startswith("http"):
path = f"/resources/media/{photo_url.lstrip('/')}"
if request:
ret["photo_url"] = request.build_absolute_uri(path)
else:
ret["photo_url"] = path
elif "localhost" in photo_url and request and "localhost" not in request.get_host():
path = photo_url.split("/resources/media/")[-1]
ret["photo_url"] = request.build_absolute_uri(f"/resources/media/{path}")
# Fallback for main photo if it's null
if not ret.get("photo") and ret.get("photo_url"):
ret["photo"] = ret["photo_url"]
# Ensure main photo is absolute if it's a relative path
photo = ret.get("photo")
if photo and isinstance(photo, str) and not photo.startswith("http"):
if not photo.startswith("/resources/"):
photo = f"/resources/media/{photo.lstrip('/')}"
if request:
ret["photo"] = request.build_absolute_uri(photo)
else:
ret["photo"] = photo
return ret
class ListCategorySerializer(BaseCategorySerializer): class ListCategorySerializer(BaseCategorySerializer):
class Meta(BaseCategorySerializer.Meta): ... class Meta(BaseCategorySerializer.Meta):
pass
class RetrieveCategorySerializer(BaseCategorySerializer): class RetrieveCategorySerializer(BaseCategorySerializer):
class Meta(BaseCategorySerializer.Meta): ... class Meta(BaseCategorySerializer.Meta):
pass
class CreateCategorySerializer(BaseCategorySerializer): class CreateCategorySerializer(BaseCategorySerializer):
class Meta(BaseCategorySerializer.Meta): class Meta(BaseCategorySerializer.Meta):
fields = [ fields = [
"id",
"firestore_id", "firestore_id",
"section", "section",
"title", "title",
"description", "description",
"photo",
"photo_url", "photo_url",
"is_publish", "is_publish",
"order", "order",

View File

@@ -15,6 +15,39 @@ class BaseSectionSerializer(serializers.ModelSerializer):
"service_type", "service_type",
] ]
def to_representation(self, instance):
ret = super().to_representation(instance)
request = self.context.get("request")
image_url = ret.get("image_url")
if image_url and isinstance(image_url, str):
if not image_url.startswith("http"):
path = f"/resources/media/{image_url.lstrip('/')}"
if request:
ret["image_url"] = request.build_absolute_uri(path)
else:
ret["image_url"] = path
elif "localhost" in image_url and request and "localhost" not in request.get_host():
path = image_url.split("/resources/media/")[-1]
ret["image_url"] = request.build_absolute_uri(f"/resources/media/{path}")
# Fallback for main image if it's null
if not ret.get("image") and ret.get("image_url"):
ret["image"] = ret["image_url"]
# Ensure main image is absolute if it's a relative path
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
return ret
class ListSectionSerializer(BaseSectionSerializer): class ListSectionSerializer(BaseSectionSerializer):
pass pass

View File

@@ -20,25 +20,60 @@ class BaseVendorSerializer(serializers.ModelSerializer):
"photo_url", "photo_url",
"is_active", "is_active",
] ]
def to_representation(self, instance):
ret = super().to_representation(instance)
request = self.context.get("request")
photo_url = ret.get("photo_url")
if photo_url and isinstance(photo_url, str):
if not photo_url.startswith("http"):
path = f"/resources/media/{photo_url.lstrip('/')}"
if request:
ret["photo_url"] = request.build_absolute_uri(path)
else:
ret["photo_url"] = path
elif "localhost" in photo_url and request and "localhost" not in request.get_host():
path = photo_url.split("/resources/media/")[-1]
ret["photo_url"] = request.build_absolute_uri(f"/resources/media/{path}")
# Fallback for main photo if it's null
if not ret.get("photo") and ret.get("photo_url"):
ret["photo"] = ret["photo_url"]
# Ensure main photo is absolute if it's a relative path
photo = ret.get("photo")
if photo and isinstance(photo, str) and not photo.startswith("http"):
if not photo.startswith("/resources/"):
photo = f"/resources/media/{photo.lstrip('/')}"
if request:
ret["photo"] = request.build_absolute_uri(photo)
else:
ret["photo"] = photo
return ret
class ListVendorSerializer(BaseVendorSerializer): class ListVendorSerializer(BaseVendorSerializer):
class Meta(BaseVendorSerializer.Meta): ... class Meta(BaseVendorSerializer.Meta):
pass
class RetrieveVendorSerializer(BaseVendorSerializer): class RetrieveVendorSerializer(BaseVendorSerializer):
class Meta(BaseVendorSerializer.Meta): ... class Meta(BaseVendorSerializer.Meta):
pass
class CreateVendorSerializer(BaseVendorSerializer): class CreateVendorSerializer(BaseVendorSerializer):
class Meta(BaseVendorSerializer.Meta): class Meta(BaseVendorSerializer.Meta):
fields = [ fields = [
"id",
"firestore_id", "firestore_id",
"section", "section",
"title", "title",
"description", "description",
"phone", "phone",
"location", "location",
"photo",
"photo_url", "photo_url",
"is_active", "is_active",
] ]

View File

@@ -8,7 +8,9 @@ class BaseProductimageSerializer(serializers.ModelSerializer):
model = ProductimageModel model = ProductimageModel
fields = [ fields = [
"id", "id",
"name", "product",
"image",
"order",
] ]
@@ -23,6 +25,7 @@ class RetrieveProductimageSerializer(BaseProductimageSerializer):
class CreateProductimageSerializer(BaseProductimageSerializer): class CreateProductimageSerializer(BaseProductimageSerializer):
class Meta(BaseProductimageSerializer.Meta): class Meta(BaseProductimageSerializer.Meta):
fields = [ fields = [
"id", "product",
"name", "image",
"order",
] ]

View File

@@ -3,9 +3,13 @@ from rest_framework import serializers
from core.apps.vendors.models import VendorproductModel, VendorModel, CategoryModel, SectionModel from core.apps.vendors.models import VendorproductModel, VendorModel, CategoryModel, SectionModel
from core.apps.vendors.serializers.vendor_product.ProductImage import ListProductimageSerializer
class BaseVendorproductSerializer(serializers.ModelSerializer): class BaseVendorproductSerializer(serializers.ModelSerializer):
category = serializers.SlugRelatedField(slug_field='firestore_id', queryset=CategoryModel.objects.all(), required=False, allow_null=True) category = serializers.SlugRelatedField(slug_field='firestore_id', queryset=CategoryModel.objects.all(), required=False, allow_null=True)
section = serializers.SlugRelatedField(slug_field='firestore_id', queryset=SectionModel.objects.all(), required=False, allow_null=True) section = serializers.SlugRelatedField(slug_field='firestore_id', queryset=SectionModel.objects.all(), required=False, allow_null=True)
images = ListProductimageSerializer(many=True, read_only=True)
class Meta: class Meta:
model = VendorproductModel model = VendorproductModel
@@ -22,30 +26,106 @@ class BaseVendorproductSerializer(serializers.ModelSerializer):
"quantity", "quantity",
"is_publish", "is_publish",
"image", "image",
"images",
"photos_json", "photos_json",
] ]
def to_representation(self, instance):
ret = super().to_representation(instance)
request = self.context.get("request")
photos = ret.get("photos_json")
# Handle photos_json list
processed_photos = []
if photos and isinstance(photos, list):
for photo in photos:
if isinstance(photo, str):
# Agar URL to'liq bo'lmasa yoki localhost bo'lsa, uni dinamik qilamiz
if not photo.startswith("http"):
path = f"/resources/media/{photo}"
if request:
processed_photos.append(request.build_absolute_uri(path))
else:
processed_photos.append(path)
elif "localhost" in photo and request and "localhost" not in request.get_host():
# Agar bazada localhost saqlangan bo'lsa-yu, biz boshqa domenda bo'lsak
path = photo.split("/resources/media/")[-1]
processed_photos.append(request.build_absolute_uri(f"/resources/media/{path}"))
else:
processed_photos.append(photo)
else:
processed_photos.append(photo)
ret["photos_json"] = processed_photos
# Fallback for main image if it's null
if not ret.get("image") and processed_photos:
ret["image"] = processed_photos[0]
# Ensure main image is absolute if it's a relative path (fallback for custom storage)
image = ret.get("image")
if image and isinstance(image, str) and not image.startswith("http"):
# Check if it already has resources prefix
if not image.startswith("/resources/"):
image = f"/resources/media/{image.lstrip('/')}"
if request:
ret["image"] = request.build_absolute_uri(image)
else:
ret["image"] = image
return ret
class ListVendorproductSerializer(BaseVendorproductSerializer): class ListVendorproductSerializer(BaseVendorproductSerializer):
class Meta(BaseVendorproductSerializer.Meta): ... class Meta(BaseVendorproductSerializer.Meta):
pass
class RetrieveVendorproductSerializer(BaseVendorproductSerializer): class RetrieveVendorproductSerializer(BaseVendorproductSerializer):
class Meta(BaseVendorproductSerializer.Meta): ... class Meta(BaseVendorproductSerializer.Meta):
pass
from core.apps.vendors.models import VendorproductModel, ProductimageModel, VendorModel, CategoryModel, SectionModel
class CreateVendorproductSerializer(BaseVendorproductSerializer): class CreateVendorproductSerializer(BaseVendorproductSerializer):
uploaded_images = serializers.ListField(
child=serializers.ImageField(max_length=1000000, allow_empty_file=False, use_url=False),
write_only=True,
required=False
)
class Meta(BaseVendorproductSerializer.Meta): class Meta(BaseVendorproductSerializer.Meta):
fields = [ fields = BaseVendorproductSerializer.Meta.fields + [
"firestore_id", "uploaded_images",
"vendor",
"category",
"section",
"name",
"description",
"price",
"discount_price",
"quantity",
"is_publish",
"photos_json",
] ]
def create(self, validated_data):
uploaded_images = validated_data.pop("uploaded_images", [])
instance = super().create(validated_data)
# Save additional gallery images
for i, img in enumerate(uploaded_images):
ProductimageModel.objects.create(
product=instance,
image=img,
order=i
)
return instance
def update(self, instance, validated_data):
uploaded_images = validated_data.pop("uploaded_images", [])
instance = super().update(instance, validated_data)
if uploaded_images:
# Optionally clear existing gallery or append?
# I'll append for now based on user's "how to upload multiple"
for i, img in enumerate(uploaded_images):
ProductimageModel.objects.create(
product=instance,
image=img,
order=instance.images.count() + i
)
return instance

View File

@@ -61,7 +61,7 @@ class VendorproductView(BaseViewSetMixin, ModelViewSet):
@extend_schema(tags=["ProductImage"]) @extend_schema(tags=["ProductImage"])
class ProductimageView(BaseViewSetMixin, ReadOnlyModelViewSet): class ProductimageView(BaseViewSetMixin, ModelViewSet):
queryset = ProductimageModel.objects.all() queryset = ProductimageModel.objects.all()
serializer_class = ListProductimageSerializer serializer_class = ListProductimageSerializer
permission_classes = [AllowAny] permission_classes = [AllowAny]

View File

@@ -1,8 +1,20 @@
from typing import Optional, Union from typing import Optional, Union
from storages.backends.s3boto3 import S3Boto3Storage
from config.env import env from config.env import env
class CustomMediaStorage(S3Boto3Storage):
"""Media fayllar uchun nisbiy URL qaytaradigan storage."""
def url(self, name, parameters=None, expire=None, http_method=None):
# Name: products/img.png -> /resources/media/products/img.png
return f"/resources/media/{name}"
class CustomStaticStorage(S3Boto3Storage):
"""Static fayllar uchun nisbiy URL qaytaradigan storage."""
def url(self, name, parameters=None, expire=None, http_method=None):
return f"/resources/static/{name}"
class Storage: class Storage:
storages = ["AWS", "MINIO", "FILE", "STATIC"] storages = ["AWS", "MINIO", "FILE", "STATIC"]
@@ -16,13 +28,15 @@ class Storage:
def get_backend(self) -> Optional[str]: def get_backend(self) -> Optional[str]:
match self.storage: match self.storage:
case "AWS" | "MINIO": case "AWS" | "MINIO":
return "storages.backends.s3boto3.S3Boto3Storage" if self.sorage_type == "default":
return "core.utils.storage.CustomMediaStorage"
return "core.utils.storage.CustomStaticStorage"
case "FILE": case "FILE":
return "django.core.files.storage.FileSystemStorage" return "django.core.files.storage.FileSystemStorage"
case "STATIC": case "STATIC":
return "django.contrib.staticfiles.storage.StaticFilesStorage" return "django.contrib.staticfiles.storage.StaticFilesStorage"
def get_options(self) -> Optional[str]: def get_options(self) -> Optional[dict]:
match self.storage: match self.storage:
case "AWS" | "MINIO": case "AWS" | "MINIO":
if self.sorage_type == "default": if self.sorage_type == "default":
@@ -31,3 +45,4 @@ class Storage:
return {"bucket_name": env.str("STORAGE_BUCKET_STATIC")} return {"bucket_name": env.str("STORAGE_BUCKET_STATIC")}
case _: case _:
return {} return {}