categorylanri import qilish qoshildi
This commit is contained in:
@@ -18,43 +18,25 @@ LOGGING = {
|
||||
"disable_existing_loggers": False,
|
||||
"formatters": {
|
||||
"verbose": {
|
||||
"format": "%(asctime)s %(name)s %(levelname)s %(pathname)s:%(lineno)d - %(message)s",
|
||||
},
|
||||
},
|
||||
"filters": {
|
||||
"exclude_errors": {
|
||||
"()": ExcludeErrorsFilter,
|
||||
"format": "%(asctime)s %(name)s %(levelname)s - %(message)s",
|
||||
},
|
||||
},
|
||||
"handlers": {
|
||||
"daily_rotating_file": {
|
||||
"level": "INFO",
|
||||
"class": "logging.handlers.TimedRotatingFileHandler",
|
||||
"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,
|
||||
"console": {
|
||||
"level": "DEBUG",
|
||||
"class": "logging.StreamHandler",
|
||||
"formatter": "verbose",
|
||||
},
|
||||
},
|
||||
"root": {
|
||||
"handlers": ["console"],
|
||||
"level": "INFO",
|
||||
},
|
||||
"loggers": {
|
||||
"django": {
|
||||
"handlers": ["daily_rotating_file", "error_file"],
|
||||
"handlers": ["console"],
|
||||
"level": "INFO",
|
||||
"propagate": True,
|
||||
},
|
||||
"root": {
|
||||
"handlers": ["daily_rotating_file", "error_file"],
|
||||
"level": "INFO",
|
||||
"propagate": True,
|
||||
"propagate": False,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
0
core/apps/shared/management/__init__.py
Normal file
0
core/apps/shared/management/__init__.py
Normal file
0
core/apps/shared/management/commands/__init__.py
Normal file
0
core/apps/shared/management/commands/__init__.py
Normal file
60
core/apps/shared/management/commands/sync_external_images.py
Normal file
60
core/apps/shared/management/commands/sync_external_images.py
Normal 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}"))
|
||||
9
core/apps/vendors/admin/category.py
vendored
9
core/apps/vendors/admin/category.py
vendored
@@ -8,6 +8,13 @@ from core.apps.vendors.models import CategoryModel
|
||||
class CategoryAdmin(ModelAdmin):
|
||||
list_display = (
|
||||
"id",
|
||||
"__str__",
|
||||
"title",
|
||||
"section",
|
||||
"photo",
|
||||
"photo_url",
|
||||
"is_publish",
|
||||
"order",
|
||||
)
|
||||
search_fields = ("title", "firestore_id")
|
||||
list_filter = ("section", "is_publish")
|
||||
|
||||
|
||||
17
core/apps/vendors/admin/vendor_product.py
vendored
17
core/apps/vendors/admin/vendor_product.py
vendored
@@ -8,13 +8,26 @@ from core.apps.vendors.models import ProductimageModel, VendorproductModel
|
||||
class VendorproductAdmin(ModelAdmin):
|
||||
list_display = (
|
||||
"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)
|
||||
class ProductimageAdmin(ModelAdmin):
|
||||
list_display = (
|
||||
"id",
|
||||
"__str__",
|
||||
"product",
|
||||
"image",
|
||||
"order",
|
||||
)
|
||||
list_filter = ("product",)
|
||||
|
||||
@@ -19,23 +19,58 @@ class BaseCategorySerializer(serializers.ModelSerializer):
|
||||
"is_publish",
|
||||
"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 Meta(BaseCategorySerializer.Meta): ...
|
||||
class Meta(BaseCategorySerializer.Meta):
|
||||
pass
|
||||
|
||||
|
||||
class RetrieveCategorySerializer(BaseCategorySerializer):
|
||||
class Meta(BaseCategorySerializer.Meta): ...
|
||||
class Meta(BaseCategorySerializer.Meta):
|
||||
pass
|
||||
|
||||
|
||||
class CreateCategorySerializer(BaseCategorySerializer):
|
||||
class Meta(BaseCategorySerializer.Meta):
|
||||
fields = [
|
||||
"id",
|
||||
"firestore_id",
|
||||
"section",
|
||||
"title",
|
||||
"description",
|
||||
"photo",
|
||||
"photo_url",
|
||||
"is_publish",
|
||||
"order",
|
||||
|
||||
33
core/apps/vendors/serializers/section/Section.py
vendored
33
core/apps/vendors/serializers/section/Section.py
vendored
@@ -15,6 +15,39 @@ class BaseSectionSerializer(serializers.ModelSerializer):
|
||||
"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):
|
||||
pass
|
||||
|
||||
39
core/apps/vendors/serializers/vendor/vendor.py
vendored
39
core/apps/vendors/serializers/vendor/vendor.py
vendored
@@ -20,25 +20,60 @@ class BaseVendorSerializer(serializers.ModelSerializer):
|
||||
"photo_url",
|
||||
"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 Meta(BaseVendorSerializer.Meta): ...
|
||||
class Meta(BaseVendorSerializer.Meta):
|
||||
pass
|
||||
|
||||
|
||||
class RetrieveVendorSerializer(BaseVendorSerializer):
|
||||
class Meta(BaseVendorSerializer.Meta): ...
|
||||
class Meta(BaseVendorSerializer.Meta):
|
||||
pass
|
||||
|
||||
|
||||
class CreateVendorSerializer(BaseVendorSerializer):
|
||||
class Meta(BaseVendorSerializer.Meta):
|
||||
fields = [
|
||||
"id",
|
||||
"firestore_id",
|
||||
"section",
|
||||
"title",
|
||||
"description",
|
||||
"phone",
|
||||
"location",
|
||||
"photo",
|
||||
"photo_url",
|
||||
"is_active",
|
||||
]
|
||||
|
||||
@@ -8,7 +8,9 @@ class BaseProductimageSerializer(serializers.ModelSerializer):
|
||||
model = ProductimageModel
|
||||
fields = [
|
||||
"id",
|
||||
"name",
|
||||
"product",
|
||||
"image",
|
||||
"order",
|
||||
]
|
||||
|
||||
|
||||
@@ -23,6 +25,7 @@ class RetrieveProductimageSerializer(BaseProductimageSerializer):
|
||||
class CreateProductimageSerializer(BaseProductimageSerializer):
|
||||
class Meta(BaseProductimageSerializer.Meta):
|
||||
fields = [
|
||||
"id",
|
||||
"name",
|
||||
"product",
|
||||
"image",
|
||||
"order",
|
||||
]
|
||||
|
||||
@@ -3,9 +3,13 @@ from rest_framework import serializers
|
||||
from core.apps.vendors.models import VendorproductModel, VendorModel, CategoryModel, SectionModel
|
||||
|
||||
|
||||
from core.apps.vendors.serializers.vendor_product.ProductImage import ListProductimageSerializer
|
||||
|
||||
|
||||
class BaseVendorproductSerializer(serializers.ModelSerializer):
|
||||
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)
|
||||
images = ListProductimageSerializer(many=True, read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = VendorproductModel
|
||||
@@ -22,30 +26,106 @@ class BaseVendorproductSerializer(serializers.ModelSerializer):
|
||||
"quantity",
|
||||
"is_publish",
|
||||
"image",
|
||||
"images",
|
||||
"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 Meta(BaseVendorproductSerializer.Meta): ...
|
||||
class Meta(BaseVendorproductSerializer.Meta):
|
||||
pass
|
||||
|
||||
|
||||
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):
|
||||
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):
|
||||
fields = [
|
||||
"firestore_id",
|
||||
"vendor",
|
||||
"category",
|
||||
"section",
|
||||
"name",
|
||||
"description",
|
||||
"price",
|
||||
"discount_price",
|
||||
"quantity",
|
||||
"is_publish",
|
||||
"photos_json",
|
||||
fields = BaseVendorproductSerializer.Meta.fields + [
|
||||
"uploaded_images",
|
||||
]
|
||||
|
||||
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
|
||||
|
||||
2
core/apps/vendors/views/vendor_product.py
vendored
2
core/apps/vendors/views/vendor_product.py
vendored
@@ -61,7 +61,7 @@ class VendorproductView(BaseViewSetMixin, ModelViewSet):
|
||||
|
||||
|
||||
@extend_schema(tags=["ProductImage"])
|
||||
class ProductimageView(BaseViewSetMixin, ReadOnlyModelViewSet):
|
||||
class ProductimageView(BaseViewSetMixin, ModelViewSet):
|
||||
queryset = ProductimageModel.objects.all()
|
||||
serializer_class = ListProductimageSerializer
|
||||
permission_classes = [AllowAny]
|
||||
|
||||
@@ -1,8 +1,20 @@
|
||||
from typing import Optional, Union
|
||||
|
||||
from storages.backends.s3boto3 import S3Boto3Storage
|
||||
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:
|
||||
|
||||
storages = ["AWS", "MINIO", "FILE", "STATIC"]
|
||||
@@ -16,13 +28,15 @@ class Storage:
|
||||
def get_backend(self) -> Optional[str]:
|
||||
match self.storage:
|
||||
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":
|
||||
return "django.core.files.storage.FileSystemStorage"
|
||||
case "STATIC":
|
||||
return "django.contrib.staticfiles.storage.StaticFilesStorage"
|
||||
|
||||
def get_options(self) -> Optional[str]:
|
||||
def get_options(self) -> Optional[dict]:
|
||||
match self.storage:
|
||||
case "AWS" | "MINIO":
|
||||
if self.sorage_type == "default":
|
||||
@@ -31,3 +45,4 @@ class Storage:
|
||||
return {"bucket_name": env.str("STORAGE_BUCKET_STATIC")}
|
||||
case _:
|
||||
return {}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user