diff --git a/.gitignore b/.gitignore index e46f1af..d20ac91 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,7 @@ poetry.lock # C extensions *.so +assets/ # Distribution / packaging .Python build/ diff --git a/config/conf/logs.py b/config/conf/logs.py index 954362c..c514491 100644 --- a/config/conf/logs.py +++ b/config/conf/logs.py @@ -30,7 +30,7 @@ LOGGING = { "daily_rotating_file": { "level": "INFO", "class": "logging.handlers.TimedRotatingFileHandler", - "filename": LOG_DIR / "django.log", + "filename": str(LOG_DIR / "django.log"), "when": "midnight", "backupCount": 30, "formatter": "verbose", @@ -39,7 +39,7 @@ LOGGING = { "error_file": { "level": "ERROR", "class": "logging.handlers.TimedRotatingFileHandler", - "filename": LOG_DIR / "django_error.log", + "filename": str(LOG_DIR / "django_error.log"), "when": "midnight", "backupCount": 30, "formatter": "verbose", diff --git a/config/conf/unfold.py b/config/conf/unfold.py index a159cd2..89b2501 100644 --- a/config/conf/unfold.py +++ b/config/conf/unfold.py @@ -90,6 +90,6 @@ UNFOLD = { "SIDEBAR": { "show_search": True, "show_all_applications": True, - "navigation": navigation.PAGES, + "navigation": navigation.PAGES, }, } diff --git a/core/apps/api/management/commands/__init__.py b/core/apps/api/management/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/core/apps/api/management/commands/import_assets.py b/core/apps/api/management/commands/import_assets.py new file mode 100644 index 0000000..17cd27d --- /dev/null +++ b/core/apps/api/management/commands/import_assets.py @@ -0,0 +1,145 @@ +import json +import os +import re +from pathlib import Path +from django.core.management.base import BaseCommand +from django.core.files import File +from django.conf import settings +from django.db import transaction +from core.apps.api.models import FilialModel, CategoryModel, SubcategoryModel, ProductsModel, SubProductModel + +class Command(BaseCommand): + help = 'Import restaurants and products from JSON assets' + + def handle(self, *args, **options): + base_dir = Path(settings.BASE_DIR) + json_dir = base_dir / 'assets' / 'json' + images_dir = base_dir / 'assets' / 'images' + + product_map = {} # Maps (filial_name, item_id) to ProductsModel instance + + # 1. Process Main Food Containers + food_container_files = sorted(json_dir.glob('FoodContainer*.json')) + + for file_path in food_container_files: + file_name = file_path.name + # Extract Filial name: FoodContainerKaromat20.json -> Karomat + match = re.search(r'FoodContainer([A-Za-z]+)(\d+)?\.json', file_name) + if not match: + self.stdout.write(self.style.WARNING(f"Skipping {file_name}, regex mismatch")) + continue + + filial_name = match.group(1) + if not filial_name: + filial_name = "Default" + + self.stdout.write(self.style.SUCCESS(f"Processing filial: {filial_name} from {file_name}")) + + # Filial can be shared across multiple JSON files + filial, _ = FilialModel.objects.get_or_create(name=filial_name) + + with open(file_path, 'r', encoding='utf-8') as f: + data = json.load(f) + + current_category = None + current_subcategory = None + + with transaction.atomic(): + for item in data: + is_header = item.get('isHeader') == 1 + name = item.get('nameRu') or item.get('nameEn') or "Unnamed" + price = item.get('price', 0) + image_name = item.get('image') + json_id = item.get('id') + + if is_header: + # Create Category and Subcategory + current_category, created = CategoryModel.objects.get_or_create( + filial=filial, + name=name + ) + if created and image_name: + self.attach_image(current_category, image_name, images_dir) + + current_subcategory, created = SubcategoryModel.objects.get_or_create( + category=current_category, + name=name + ) + self.stdout.write(f" Category/Subcategory: {name}") + else: + if not current_subcategory: + # Fallback if no header found yet + cat_name = "General" + current_category, _ = CategoryModel.objects.get_or_create( + filial=filial, + name=cat_name + ) + current_subcategory, _ = SubcategoryModel.objects.get_or_create( + category=current_category, + name=cat_name + ) + + # Create Product + # We use get_or_create with name and subcategory to avoid obvious duplicates + product, created = ProductsModel.objects.get_or_create( + name=name, + subcategory=current_subcategory, + defaults={'price': price if price > 0 else 0} + ) + if created: + if image_name: + self.attach_image(product, image_name, images_dir) + self.stdout.write(f" Added Product: {name}") + + # Store in map (scoped by filial and ID) for subproduct linking + product_map[(filial_name, json_id)] = product + + # 2. Process Food Detail Containers (Subproducts) + food_detail_files = sorted(json_dir.glob('FoodDetailContainer*.json')) + for file_path in food_detail_files: + file_name = file_path.name + match = re.search(r'FoodDetailContainer(.*?)(?:\d+)?\.json', file_name) + if not match: + continue + + filial_name = match.group(1) + self.stdout.write(self.style.SUCCESS(f"Processing subproducts for filial: {filial_name} from {file_name}")) + + with open(file_path, 'r', encoding='utf-8') as f: + data = json.load(f) + + with transaction.atomic(): + for item in data: + food_id = item.get('foodId') + name = item.get('nameEn') or item.get('nameRu') or "Unnamed Subproduct" + price = item.get('price', 0) + + product_key = (filial_name, food_id) + if product_key in product_map: + parent_product = product_map[product_key] + sub_product, created = SubProductModel.objects.get_or_create( + product=parent_product, + name=name, + defaults={'price': price if price > 0 else 0} + ) + if created: + self.stdout.write(f" Added SubProduct: {name} for {parent_product.name}") + else: + # Sometimes subproducts might refer to products from other files or just missing + pass + + def attach_image(self, instance, image_name, images_dir): + # Try different extensions + if not image_name: + return False + + for ext in ['.jpg', '.png', '.jpeg', '.webp']: + img_path = images_dir / (image_name + ext) + if img_path.exists(): + try: + with open(img_path, 'rb') as img_file: + instance.image.save(img_path.name, File(img_file), save=True) + return True + except Exception as e: + self.stdout.write(self.style.ERROR(f" Error saving image {image_name}: {e}")) + return False