diff --git "a/app.py" "b/app.py" --- "a/app.py" +++ "b/app.py" @@ -28,9 +28,6 @@ HF_TOKEN_READ = os.getenv("HF_TOKEN_READ") STORE_ADDRESS = "Город Алматы, Рынок Олжа, ряд VIP, Бутик 7" -CURRENCY_CODE = 'USD' -CURRENCY_NAME = 'доллар' - DOWNLOAD_RETRIES = 3 DOWNLOAD_DELAY = 5 @@ -38,10 +35,11 @@ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %( translations = { 'ru': { - 'site_title': "Manolisa - Каталог", - 'brand_name': "Manolisa", + 'site_title': "ManolisA - Каталог", + 'brand_name': "ManolisA", 'our_address': "Наш адрес:", 'all_categories': "Все категории", + 'all_seasons': "Все сезоны", 'search_placeholder': "Поиск по названию или описанию...", 'no_products_added': "Товары пока не добавлены.", 'no_products_found': "По вашему запросу товары не найдены.", @@ -52,7 +50,7 @@ translations = { 'details': "Подробнее", 'add_to_cart': "В корзину", 'loading': "Загрузка...", - 'specify_quantity_color': "Укажите количество и цвет", + 'specify_quantity_color': "Укажите количество", 'quantity': "Количество (линеек):", 'color_variant': "Цвет/Вариант:", 'confirm_add_to_cart': "Добавить в корзину", @@ -73,6 +71,8 @@ translations = { 'order_creation_error': "Ошибка при формировании заказа:", 'category': "Категория:", 'no_category': "Без категории", + 'season': "Сезон:", + 'no_season': "Без сезона", 'price': "Цена:", 'description': "Описание:", 'no_description': "Описание отсутствует.", @@ -86,7 +86,7 @@ translations = { 'order_status_desc': "Этот заказ был оформлен без входа в систему. Пожалуйста, свяжитесь с нами по WhatsApp для подтверждения и уточнения деталей.", 'send_order': "Отправить заказ", 'back_to_catalog': "← Вернуться в каталог", - 'whatsapp_greeting': "Здравствуйте! Хочу подтвердить свой заказ на Manolisa:", + 'whatsapp_greeting': "Здравствуйте! Хочу подтвердить свой заказ на ManolisA:", 'whatsapp_order_number': "Номер заказа:", 'whatsapp_order_link': "Ссылка на заказ:", 'whatsapp_contact_me': "Пожалуйста, свяжитесь со мной для уточнения деталей оплаты и доставки.", @@ -101,12 +101,15 @@ translations = { 'online_order': 'Онлайн', 'address_1_title': 'Адрес:', 'address_1_detail': 'Город Алматы, Рынок Олжа, ряд VIP, Бутик 7', + 'currency_usd': '$', + 'currency_kzt': '₸', }, 'kk': { - 'site_title': "Manolisa - Каталог", - 'brand_name': "Manolisa", + 'site_title': "ManolisA - Каталог", + 'brand_name': "ManolisA", 'our_address': "Біздің мекенжайымыз:", 'all_categories': "Барлық санаттар", + 'all_seasons': "Барлық маусымдар", 'search_placeholder': "Аты немесе сипаттамасы бойынша іздеу...", 'no_products_added': "Тауарлар әлі қосылмаған.", 'no_products_found': "Сіздің сұранысыңыз бойынша тауарлар табылмады.", @@ -117,7 +120,7 @@ translations = { 'details': "Толығырақ", 'add_to_cart': "Себетке қосу", 'loading': "Жүктелуде...", - 'specify_quantity_color': "Саны мен түсін көрсетіңіз", + 'specify_quantity_color': "Санын көрсетіңіз", 'quantity': "Саны (лента):", 'color_variant': "Түсі/нұсқасы:", 'confirm_add_to_cart': "Себетке қосу", @@ -138,6 +141,8 @@ translations = { 'order_creation_error': "Тапсырысты рәсімдеу кезінде қате:", 'category': "Санат:", 'no_category': "Санатсыз", + 'season': "Маусым:", + 'no_season': "Маусымсыз", 'price': "Бағасы:", 'description': "Сипаттамасы:", 'no_description': "Сипаттамасы жоқ.", @@ -151,7 +156,7 @@ translations = { 'order_status_desc': "Бұл тапсырыс жүйеге кірмей рәсімделді. Растау және мәліметтерді нақтылау үшін WhatsApp арқылы бізбен хабарласыңыз.", 'send_order': "Тапсырысты жіберу", 'back_to_catalog': "← Каталогқа оралу", - 'whatsapp_greeting': "Сәлеметсіз бе! Мен Manolisa-дегі тапсырысымды растағым келеді:", + 'whatsapp_greeting': "Сәлеметсіз бе! Мен ManolisA-дегі тапсырысымды растағым келеді:", 'whatsapp_order_number': "Тапсырыс нөмірі:", 'whatsapp_order_link': "Тапсырысқа сілтеме:", 'whatsapp_contact_me': "Төлем және жеткізу мәліметтерін нақтылау үшін менімен хабарласыңыз.", @@ -166,6 +171,8 @@ translations = { 'online_order': 'Онлайн', 'address_1_title': 'Мекенжай:', 'address_1_detail': 'Алматы қаласы, Олжа базары, VIP қатары, 7 бутик', + 'currency_usd': '$', + 'currency_kzt': '₸', } } @@ -225,7 +232,7 @@ def download_db_from_hf(specific_file=None, retries=DOWNLOAD_RETRIES, delay=DOWN try: if file_name == DATA_FILE: with open(file_name, 'w', encoding='utf-8') as f: - json.dump({'products': [], 'categories': [], 'orders': {}, 'employees': []}, f) + json.dump({'products': [], 'categories': [], 'seasons': [], 'orders': {}, 'employees': [], 'settings': {'usd_kzt_rate': 450.0}}, f) logging.info(f"Created empty local file {file_name} because it was not found on HF.") except Exception as create_e: logging.error(f"Failed to create empty local file {file_name}: {create_e}") @@ -286,10 +293,28 @@ def periodic_backup(): upload_db_to_hf() logging.info("Periodic backup finished.") - +def migrate_data_structure(data): + migrated = False + if 'products' in data and isinstance(data['products'], list): + for product in data['products']: + if 'photos' in product and 'variants' not in product: + migrated = True + colors = product.pop('colors', []) + photos = product.pop('photos', []) + product['variants'] = [] + + if colors and any(c.strip() for c in colors): + for color in colors: + if color.strip(): + product['variants'].append({"color": color.strip(), "photos": list(photos)}) + else: + product['variants'].append({"color": "Default", "photos": photos}) + if migrated: + logging.info("Migrated old product data structure to new variant-based structure.") + return data, migrated def load_data(): - default_data = {'products': [], 'categories': [], 'orders': {}, 'employees': []} + default_data = {'products': [], 'categories': [], 'seasons': [], 'orders': {}, 'employees': [], 'settings': {'usd_kzt_rate': 450.0}} try: with open(DATA_FILE, 'r', encoding='utf-8') as file: data = json.load(file) @@ -297,11 +322,21 @@ def load_data(): if not isinstance(data, dict): logging.warning(f"Local {DATA_FILE} is not a dictionary. Attempting download.") raise FileNotFoundError + + # Check for all keys if 'products' not in data: data['products'] = [] if 'categories' not in data: data['categories'] = [] + if 'seasons' not in data: data['seasons'] = [] if 'orders' not in data: data['orders'] = {} if 'employees' not in data: data['employees'] = [] + if 'settings' not in data: data['settings'] = {'usd_kzt_rate': 450.0} + if 'usd_kzt_rate' not in data['settings']: data['settings']['usd_kzt_rate'] = 450.0 + + data, migrated = migrate_data_structure(data) + if migrated: + save_data(data) return data + except FileNotFoundError: logging.warning(f"Local file {DATA_FILE} not found. Attempting download from HF.") @@ -313,11 +348,20 @@ def load_data(): if not isinstance(data, dict): logging.error(f"Downloaded {DATA_FILE} is not a dictionary. Using default.") return default_data + if 'products' not in data: data['products'] = [] if 'categories' not in data: data['categories'] = [] + if 'seasons' not in data: data['seasons'] = [] if 'orders' not in data: data['orders'] = {} if 'employees' not in data: data['employees'] = [] + if 'settings' not in data: data['settings'] = {'usd_kzt_rate': 450.0} + if 'usd_kzt_rate' not in data['settings']: data['settings']['usd_kzt_rate'] = 450.0 + + data, migrated = migrate_data_structure(data) + if migrated: + save_data(data) return data + except FileNotFoundError: logging.error(f"File {DATA_FILE} still not found even after download reported success. Using default.") return default_data @@ -343,11 +387,7 @@ def save_data(data): if not isinstance(data, dict): logging.error("Attempted to save invalid data structure (not a dict). Aborting save.") return - if 'products' not in data: data['products'] = [] - if 'categories' not in data: data['categories'] = [] - if 'orders' not in data: data['orders'] = {} - if 'employees' not in data: data['employees'] = [] - + with open(DATA_FILE, 'w', encoding='utf-8') as file: json.dump(data, file, ensure_ascii=False, indent=4) logging.info(f"Data successfully saved to {DATA_FILE}") @@ -365,84 +405,90 @@ CATALOG_TEMPLATE = ''' {{ _('site_title') }} - + @@ -1143,18 +1239,18 @@ ORDER_TEMPLATE = '''
{{ item.name }}
- {{ item.name }} {% if item.color != 'N/A' %}({{ item.color }}){% endif %} - {{ item.quantity }} × {{ "%.2f"|format(item.price) }} {{ currency_code }} + {{ item.name }} {% if item.color and item.color != 'Default' %}({{ item.color }}){% endif %} + {{ item.quantity }} × {{ "%.2f"|format(item.price) }} USD
- {{ "%.2f"|format(item.price * item.quantity) }} {{ currency_code }} + {{ "%.2f"|format(item.price * item.quantity) }} USD
{% endfor %}
-

{{ _('total_to_pay') }}: {{ "%.2f"|format(order.total_price) }} {{ currency_code }}

+

{{ _('total_to_pay') }}: {{ "%.2f"|format(order.total_price) }} USD

@@ -1185,7 +1281,7 @@ ORDER_TEMPLATE = ''' {% else %} -

{{ _('error') }}

+

{{ _('error') }}

{{ _('order_not_found') }}

{{ _('back_to_catalog') }} {% endif %} @@ -1200,383 +1296,236 @@ ADMIN_TEMPLATE = ''' - Админ-панель - Manolisa - + Админ-панель - ManolisA +
-
- -

Админ-панель Manolisa

+
+ ManolisA Logo +

Админ-панель ManolisA

- Перейти в катал��г + Перейти в каталог
- {% with messages = get_flashed_messages(with_categories=true) %} - {% if messages %} - {% for category, message in messages %} -
{{ message }}
- {% endfor %} - {% endif %} + {% if messages %}{% for category, message in messages %} +
{{ message }}
+ {% endfor %}{% endif %} {% endwith %} + +
+

Общие настройки

+
+ + + + +
+
-

Синхронизация с Датацентром

+

Синхронизация

-
- + +
-
- + +
-

Резервное копирование происходит автоматически каждые 30 минут, а также после каждого сохранения данных. Используйте эти кнопки для немедленной синхронизации.

-
-
-

Управление категориями

-
- Добавить новую категорию -
-
- - - - -
-
-
- -

Существующие категории:

- {% if categories %} -
- {% for category in categories %} -
- {{ category }} -
- - - -
-
- {% endfor %} -
- {% else %} -

Категорий пока нет.

- {% endif %} -
+
+

Категории

+
Добавить категорию
+
+
+

Существующие:

+
{% for category in categories %}
{{ category }}
{% else %}

Категорий нет.

{% endfor %}
- -
-
-

Управление сотрудниками

-
- Добавить нового сотрудника -
-
- - - - -
-
-
- -

Список сотрудников:

- {% if employees %} -
- {% for employee in employees %} -
- {{ employee }} -
- - - -
-
- {% endfor %} -
- {% else %} -

Сотрудников пока нет.

- {% endif %} -
+
+

Сезоны

+
Добавить сезон
+
+
+

Существующие:

+
{% for season in seasons %}
{{ season }}
{% else %}

Сезонов нет.

{% endfor %}
+
+
+

Сотрудники

+
Добавить сотрудника
+
+
+

Список:

+
{% for employee in employees %}
{{ employee }}
{% else %}

Сотрудников нет.

{% endfor %}
-

Управление товарами

+

Товары

Добавить новый товар
- - - - - - - - - - - - - -
-
- - -
-
- -
-
- - -
-
- - -
-
- + + + + + + +
+

Цвета и Фотографии

+
+ +
+
+
+

Список товаров:

- {% if products %}
{% for product in products %}
-
- {% if product.get('photos') %} - - Фото - - {% else %} - Нет фото - {% endif %} +
+ {% set first_photo = product.variants[0].photos[0] if product.variants and product.variants[0].photos else none %} + {% if first_photo %} + Фото + {% else %}Нет фото{% endif %}
-

- {{ product['name'] }} - {% if product.get('in_stock', True) %} - В наличии - {% else %} - Нет в наличии - {% endif %} - {% if product.get('is_top', False) %} - Топ - {% endif %} -

-

Категория: {{ product.get('category', 'Без категории') }}

-

Цена за линейку: {{ "%.2f"|format(product['price']) }} {{ currency_code }}

-

Шт. в линейке: {{ product.get('items_per_line', 'N/A') }}

-

Описание: {{ product.get('description', 'N/A')[:150] }}{% if product.get('description', '')|length > 150 %}...{% endif %}

- {% set colors = product.get('colors', []) %} -

Цвета/Вар-ты: {{ colors|select('ne', '')|join(', ') if colors|select('ne', '')|list|length > 0 else 'Нет' }}

- {% if product.get('photos') and product['photos']|length > 1 %} -

(Всего фото: {{ product['photos']|length }})

- {% endif %} +

{{ product['name'] }} + {% if product.get('in_stock', True) %}В наличии{% else %}Нет{% endif %} + {% if product.get('is_top', False) %} Топ{% endif %} +

+

Категория: {{ product.get('category', 'N/A') }}

+

Сезон: {{ product.get('season', 'N/A') }}

+

Цена: {{ "%.2f"|format(product['price']) }} USD ({{ product.get('items_per_line', 'N/A') }} шт.)

+

Цвета: {{ product.variants|map(attribute='color')|join(', ') }}

-
- -
- - + + +
-
-

Редактирование: {{ product['name'] }}

+

Редактирование: {{ product['name'] }}

- - - - - - - - - - - - - - - {% if product.get('photos') %} -

Текущие фото:

-
- {% for photo in product['photos'] %} - Фото {{ loop.index }} - {% endfor %} + + + + + + + +
+

Цвета и Фотографии

+
+ {% for variant in product.variants %} +
+ + + + + +
{% for p in variant.photos %}{% endfor %}
+
- {% endif %} - -
- {% set current_colors = product.get('colors', []) %} - {% if current_colors and current_colors|select('ne', '')|list|length > 0 %} - {% for color in current_colors %} - {% if color.strip() %} -
- - -
- {% endif %} - {% endfor %} - {% else %} -
- - -
- {% endif %} -
- -
-
- - -
-
- - + {% endfor %}
-
- + +
+ +
+ +
+
{% endfor %}
- {% else %} -

Товаров пока нет.

- {% endif %}
-
- @@ -1589,7 +1538,9 @@ def catalog(): data = load_data() all_products = data.get('products', []) categories = sorted(data.get('categories', [])) + seasons = sorted(data.get('seasons', [])) employees = sorted(data.get('employees', [])) + usd_kzt_rate = data.get('settings', {}).get('usd_kzt_rate', 450.0) needs_save = False for product in all_products: @@ -1607,10 +1558,11 @@ def catalog(): CATALOG_TEMPLATE, products=products_sorted, categories=categories, + seasons=seasons, employees=employees, repo_id=REPO_ID, store_address=STORE_ADDRESS, - currency_code=CURRENCY_CODE + usd_kzt_rate=usd_kzt_rate ) @app.route('/product/') @@ -1626,14 +1578,12 @@ def product_detail(index): try: product = products_sorted[index] except IndexError: - logging.warning(f"Attempted access to non-existent or out-of-stock product with index {index}") return _("product_not_found"), 404 return render_template_string( PRODUCT_DETAIL_TEMPLATE, product=product, - repo_id=REPO_ID, - currency_code=CURRENCY_CODE + repo_id=REPO_ID ) @app.route('/create_order', methods=['POST']) @@ -1641,430 +1591,249 @@ def create_order(): order_data = request.get_json() if not order_data or 'cart' not in order_data or not order_data['cart']: - logging.warning("Create order request missing cart data or cart is empty.") - return jsonify({"error": "Корзина пуста или не передана."}), 400 + return jsonify({"error": "Корзина пуста."}), 400 cart_items = order_data['cart'] employee_name = order_data.get('employee', 'Онлайн') - - total_price = 0 + total_price = sum(float(item['price']) * int(item['quantity']) for item in cart_items) + processed_cart = [] for item in cart_items: - if not all(k in item for k in ('name', 'price', 'quantity')): - logging.error(f"Invalid cart item structure received: {item}") - return jsonify({"error": "Неверный формат товара в корзине."}), 400 - try: - price = float(item['price']) - quantity = int(item['quantity']) - if price < 0 or quantity <= 0: - raise ValueError("Invalid price or quantity") - total_price += price * quantity - processed_cart.append({ - "name": item['name'], - "price": price, - "quantity": quantity, - "color": item.get('color', 'N/A'), - "photo": item.get('photo'), - "items_per_line": item.get('items_per_line'), - "photo_url": f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/photos/{item['photo']}" if item.get('photo') else "https://via.placeholder.com/70x70.png?text=N/A" - }) - except (ValueError, TypeError) as e: - logging.error(f"Invalid price/quantity in cart item: {item}. Error: {e}") - return jsonify({"error": "Неверная цена или количество в товаре."}), 400 - - order_id = f"{datetime.now().strftime('%Y%m%d%H%M%S')}-{uuid.uuid4().hex[:6]}" - order_timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S') - + photo_url = f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/photos/{item['photo']}" if item.get('photo') else "https://via.placeholder.com/70x70.png?text=N/A" + processed_cart.append({ + "name": item['name'], "price": item['price'], "quantity": item['quantity'], + "color": item.get('color', 'N/A'), "photo": item.get('photo'), + "items_per_line": item.get('items_per_line'), "photo_url": photo_url + }) + + order_id = f"{datetime.now().strftime('%y%m%d%H%M')}-{uuid.uuid4().hex[:4]}" new_order = { - "id": order_id, - "created_at": order_timestamp, - "cart": processed_cart, - "total_price": round(total_price, 2), - "employee": employee_name, - "status": "new" + "id": order_id, "created_at": datetime.now().strftime('%Y-%m-%d %H:%M:%S'), + "cart": processed_cart, "total_price": round(total_price, 2), + "employee": employee_name, "status": "new" } try: data = load_data() - if 'orders' not in data or not isinstance(data.get('orders'), dict): - data['orders'] = {} - data['orders'][order_id] = new_order save_data(data) - logging.info(f"Order {order_id} created successfully by {employee_name}.") return jsonify({"order_id": order_id}), 201 - except Exception as e: logging.error(f"Failed to save order {order_id}: {e}", exc_info=True) - return jsonify({"error": "Ошибка сервера при сохранении заказа."}), 500 + return jsonify({"error": "Ошибка сервера."}), 500 @app.route('/order/') def view_order(order_id): data = load_data() order = data.get('orders', {}).get(order_id) - - if order: - logging.info(f"Displaying order {order_id}") - else: - logging.warning(f"Order {order_id} not found.") - - return render_template_string(ORDER_TEMPLATE, - order=order, - repo_id=REPO_ID, - currency_code=CURRENCY_CODE) + return render_template_string(ORDER_TEMPLATE, order=order, repo_id=REPO_ID) + +def upload_photos_to_hf(files, product_name): + if not files or not HF_TOKEN_WRITE: return [] + photo_list = [] + uploads_dir = 'uploads_temp' + os.makedirs(uploads_dir, exist_ok=True) + api = HfApi() + for photo in files: + if photo and photo.filename: + try: + ext = os.path.splitext(photo.filename)[1].lower() + if ext not in ['.jpg', '.jpeg', '.png', '.gif', '.webp']: continue + safe_name = secure_filename(product_name.replace(' ', '_'))[:50] + filename = f"{safe_name}_{uuid.uuid4().hex[:8]}{ext}" + temp_path = os.path.join(uploads_dir, filename) + photo.save(temp_path) + api.upload_file( + path_or_fileobj=temp_path, path_in_repo=f"photos/{filename}", + repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE + ) + photo_list.append(filename) + os.remove(temp_path) + except Exception as e: + logging.error(f"Error uploading photo {photo.filename}: {e}") + if os.path.exists(uploads_dir) and not os.listdir(uploads_dir): os.rmdir(uploads_dir) + return photo_list @app.route('/admin', methods=['GET', 'POST']) def admin(): data = load_data() - products = data.get('products', []) - categories = data.get('categories', []) - employees = data.get('employees', []) - - needs_save = False - for product in products: - if 'id' not in product: - product['id'] = str(uuid.uuid4()) - needs_save = True - if needs_save: - logging.info("Assigning unique IDs to products that were missing them.") - data['products'] = products - save_data(data) - + if request.method == 'POST': action = request.form.get('action') - logging.info(f"Admin action received: {action}") - - try: - if action == 'add_category': - category_name = request.form.get('category_name', '').strip() - if category_name and category_name not in categories: - categories.append(category_name) - data['categories'] = categories - save_data(data) - logging.info(f"Category '{category_name}' added.") - flash(f"Категория '{category_name}' успешно добавлена.", 'success') - elif not category_name: - logging.warning("Attempted to add empty category.") - flash("Название категории не может быть пустым.", 'error') - else: - logging.warning(f"Category '{category_name}' already exists.") - flash(f"Категория '{category_name}' уже существует.", 'error') - - elif action == 'delete_category': - category_to_delete = request.form.get('category_name') - if category_to_delete and category_to_delete in categories: - categories.remove(category_to_delete) - updated_count = 0 - for product in products: - if product.get('category') == category_to_delete: - product['category'] = 'Без категории' - updated_count += 1 - data['categories'] = categories - data['products'] = products - save_data(data) - logging.info(f"Category '{category_to_delete}' deleted. Updated products: {updated_count}.") - flash(f"Категория '{category_to_delete}' удалена. {updated_count} товаров обновлено.", 'success') - else: - logging.warning(f"Attempted to delete non-existent or empty category: {category_to_delete}") - flash(f"Не удалось удалить категорию '{category_to_delete}'.", 'error') - - elif action == 'add_employee': - employee_name = request.form.get('employee_name', '').strip() - if employee_name and employee_name not in employees: - employees.append(employee_name) - data['employees'] = employees - save_data(data) - flash(f"Сотрудник '{employee_name}' успешно добавлен.", 'success') - elif not employee_name: - flash("Имя сотрудника не может быть пустым.", 'error') - else: - flash(f"Сотрудник '{employee_name}' уже существует.", 'error') - - elif action == 'delete_employee': - employee_to_delete = request.form.get('employee_name') - if employee_to_delete and employee_to_delete in employees: - employees.remove(employee_to_delete) - data['employees'] = employees - save_data(data) - flash(f"Сотрудник '{employee_to_delete}' удален.", 'success') - else: - flash(f"Не удалось удалить сотрудника '{employee_to_delete}'.", 'error') - - elif action == 'add_product': - name = request.form.get('name', '').strip() - price_str = request.form.get('price', '').replace(',', '.') - items_per_line_str = request.form.get('items_per_line', '1') - description = request.form.get('description', '').strip() - category = request.form.get('category') - photos_files = request.files.getlist('photos') - colors = [c.strip() for c in request.form.getlist('colors') if c.strip()] - in_stock = 'in_stock' in request.form - is_top = 'is_top' in request.form - - if not name or not price_str or not items_per_line_str: - flash("Название, цена и кол-во в линейке обязательны.", 'error') - return redirect(url_for('admin')) - - try: - price = round(float(price_str), 2) - items_per_line = int(items_per_line_str) - if price < 0 or items_per_line <= 0: - raise ValueError - except ValueError: - flash("Неверный формат цены или кол-ва в линейке.", 'error') - return redirect(url_for('admin')) - - photos_list = [] - if photos_files and HF_TOKEN_WRITE: - uploads_dir = 'uploads_temp' - os.makedirs(uploads_dir, exist_ok=True) - api = HfApi() - photo_limit = 10 - uploaded_count = 0 - for photo in photos_files: - if uploaded_count >= photo_limit: - logging.warning(f"Photo limit ({photo_limit}) reached, ignoring remaining photos.") - flash(f"Загружено только первые {photo_limit} фото.", "warning") - break - if photo and photo.filename: - try: - ext = os.path.splitext(photo.filename)[1].lower() - if ext not in ['.jpg', '.jpeg', '.png', '.gif', '.webp']: - logging.warning(f"Skipping non-image file upload: {photo.filename}") - flash(f"Файл {photo.filename} не является изображением и был пропущен.", "warning") - continue - - safe_name = secure_filename(name.replace(' ', '_'))[:50] - photo_filename = f"{safe_name}_{datetime.now().strftime('%Y%m%d%H%M%S%f')}{ext}" - temp_path = os.path.join(uploads_dir, photo_filename) - photo.save(temp_path) - logging.info(f"Uploading photo {photo_filename} to HF for product {name}...") - api.upload_file( - path_or_fileobj=temp_path, path_in_repo=f"photos/{photo_filename}", - repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE, - commit_message=f"Add photo for product {name}" - ) - photos_list.append(photo_filename) - logging.info(f"Photo {photo_filename} uploaded successfully.") - os.remove(temp_path) - uploaded_count += 1 - except Exception as e: - logging.error(f"Error uploading photo {photo.filename} to HF: {e}", exc_info=True) - flash(f"Ошибка при загрузке фото {photo.filename}.", 'error') - if os.path.exists(temp_path): - try: os.remove(temp_path) - except OSError: pass - elif photo and not photo.filename: - logging.warning("Received an empty photo file object when adding product.") - try: - if os.path.exists(uploads_dir) and not os.listdir(uploads_dir): - os.rmdir(uploads_dir) - except OSError as e: - logging.warning(f"Could not remove temporary upload directory {uploads_dir}: {e}") - elif not HF_TOKEN_WRITE and photos_files and any(f.filename for f in photos_files): - flash("HF_TOKEN (write) не настроен. Фотографии не были загружены.", "warning") - - - new_product = { - 'id': str(uuid.uuid4()), - 'name': name, 'price': price, 'description': description, - 'category': category if category in categories else 'Без категории', - 'photos': photos_list, 'colors': colors, - 'in_stock': in_stock, 'is_top': is_top, - 'items_per_line': items_per_line - } - products.append(new_product) - data['products'] = products + + # Settings + if action == 'update_settings': + rate = request.form.get('usd_kzt_rate') + try: + data['settings']['usd_kzt_rate'] = float(rate) save_data(data) - logging.info(f"Product '{name}' added.") - flash(f"Товар '{name}' успешно добавлен.", 'success') - - elif action == 'edit_product': - product_id = request.form.get('product_id') - if not product_id: - flash("Ошибка редактирования: ID товара не передан.", 'error') - return redirect(url_for('admin')) + flash('Курс валют обновлен.', 'success') + except (ValueError, TypeError): + flash('Неверный формат курса.', 'error') + + # Categories + elif action == 'add_category': + name = request.form.get('category_name', '').strip() + if name and name not in data['categories']: + data['categories'].append(name) + save_data(data) + flash(f"Категория '{name}' добавлена.", 'success') + elif action == 'delete_category': + name = request.form.get('category_name') + if name in data['categories']: + data['categories'].remove(name) + for p in data['products']: + if p.get('category') == name: p['category'] = '' + save_data(data) + flash(f"Категория '{name}' удалена.", 'success') + + # Seasons + elif action == 'add_season': + name = request.form.get('season_name', '').strip() + if name and name not in data['seasons']: + data['seasons'].append(name) + save_data(data) + flash(f"Сезон '{name}' добавлен.", 'success') + elif action == 'delete_season': + name = request.form.get('season_name') + if name in data['seasons']: + data['seasons'].remove(name) + for p in data['products']: + if p.get('season') == name: p['season'] = '' + save_data(data) + flash(f"Сезон '{name}' удален.", 'success') - product_index = next((i for i, p in enumerate(products) if p.get('id') == product_id), -1) + # Employees + elif action == 'add_employee': + name = request.form.get('employee_name', '').strip() + if name and name not in data['employees']: + data['employees'].append(name) + save_data(data) + flash(f"Сотрудник '{name}' добавлен.", 'success') + elif action == 'delete_employee': + name = request.form.get('employee_name') + if name in data['employees']: + data['employees'].remove(name) + save_data(data) + flash(f"Сотрудник '{name}' удален.", 'success') - if product_index == -1: - flash(f"Ошибка редактирования: товар с ID '{product_id}' не найден.", 'error') - logging.error(f"Invalid product ID '{product_id}' for editing.") + # Products + elif action in ['add_product', 'edit_product']: + try: + name = request.form.get('name').strip() + price = round(float(request.form.get('price')), 2) + items_per_line = int(request.form.get('items_per_line')) + + variants = [] + i = 0 + while f'variant-color-{i}' in request.form: + color = request.form.get(f'variant-color-{i}').strip() + if not color: + i += 1 + continue + + photos = [] + # For editing, check existing photos + if action == 'edit_product': + product_id = request.form.get('product_id') + product = next((p for p in data['products'] if p.get('id') == product_id), None) + if product: + variant_id = request.form.get(f'variant-id-{i}') + existing_variant = next((v for v in product.get('variants', []) if v.get('color') == variant_id), None) + if existing_variant: + photos = existing_variant.get('photos', []) + + new_photos = request.files.getlist(f'variant-photos-{i}') + if new_photos and any(f.filename for f in new_photos): + if action == 'edit_product' and photos: # Delete old photos if replacing + if HF_TOKEN_WRITE: HfApi().delete_files(repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE, paths_in_repo=[f"photos/{p}" for p in photos]) + photos = upload_photos_to_hf(new_photos, name) + + variants.append({'color': color, 'photos': photos}) + i += 1 + + if not variants: + flash("Нужно добавить хотя бы один вариант цвета/фото.", 'error') return redirect(url_for('admin')) - product_to_edit = products[product_index] - original_name = product_to_edit.get('name', 'N/A') + product_data = { + 'name': name, 'price': price, 'items_per_line': items_per_line, + 'description': request.form.get('description', '').strip(), + 'category': request.form.get('category', ''), + 'season': request.form.get('season', ''), + 'in_stock': 'in_stock' in request.form, + 'is_top': 'is_top' in request.form, + 'variants': variants + } - product_to_edit['name'] = request.form.get('name', product_to_edit['name']).strip() - price_str = request.form.get('price', str(product_to_edit['price'])).replace(',', '.') - items_per_line_str = request.form.get('items_per_line', str(product_to_edit.get('items_per_line', '1'))) - product_to_edit['description'] = request.form.get('description', product_to_edit.get('description', '')).strip() - category = request.form.get('category') - product_to_edit['category'] = category if category in categories else 'Без категории' - product_to_edit['colors'] = [c.strip() for c in request.form.getlist('colors') if c.strip()] - product_to_edit['in_stock'] = 'in_stock' in request.form - product_to_edit['is_top'] = 'is_top' in request.form + if action == 'add_product': + product_data['id'] = str(uuid.uuid4()) + data['products'].append(product_data) + flash(f"Товар '{name}' добавлен.", 'success') + else: # edit_product + product_id = request.form.get('product_id') + idx = next((i for i, p in enumerate(data['products']) if p.get('id') == product_id), -1) + if idx != -1: + product_data['id'] = product_id + data['products'][idx] = product_data + flash(f"Товар '{name}' обновлен.", 'success') + + save_data(data) - try: - price = round(float(price_str), 2) - items_per_line = int(items_per_line_str) - if price < 0 or items_per_line <= 0: raise ValueError - product_to_edit['price'] = price - product_to_edit['items_per_line'] = items_per_line - except ValueError: - logging.warning(f"Invalid price/line format during edit of product {original_name}. Not changed.") - flash(f"Неверный формат цены/кол-ва для товара '{original_name}'. Не изменено.", 'warning') - - photos_files = request.files.getlist('photos') - if photos_files and any(f.filename for f in photos_files) and HF_TOKEN_WRITE: - uploads_dir = 'uploads_temp' - os.makedirs(uploads_dir, exist_ok=True) + except Exception as e: + flash(f"Ошибка при обработке товара: {e}", 'error') + logging.error(f"Error processing product: {e}", exc_info=True) + + elif action == 'delete_product': + product_id = request.form.get('product_id') + product = next((p for p in data['products'] if p.get('id') == product_id), None) + if product: + data['products'] = [p for p in data['products'] if p.get('id') != product_id] + if HF_TOKEN_WRITE: api = HfApi() - new_photos_list = [] - photo_limit = 10 - uploaded_count = 0 - logging.info(f"Uploading new photos for product {product_to_edit['name']}...") - for photo in photos_files: - if uploaded_count >= photo_limit: - break - if photo and photo.filename: - try: - ext = os.path.splitext(photo.filename)[1].lower() - if ext not in ['.jpg', '.jpeg', '.png', '.gif', '.webp']: continue - - safe_name = secure_filename(product_to_edit['name'].replace(' ', '_'))[:50] - photo_filename = f"{safe_name}_{datetime.now().strftime('%Y%m%d%H%M%S%f')}{ext}" - temp_path = os.path.join(uploads_dir, photo_filename) - photo.save(temp_path) - api.upload_file(path_or_fileobj=temp_path, path_in_repo=f"photos/{photo_filename}", - repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE, - commit_message=f"Update photo for product {product_to_edit['name']}") - new_photos_list.append(photo_filename) - os.remove(temp_path) - uploaded_count += 1 - except Exception as e: - logging.error(f"Error uploading new photo {photo.filename}: {e}", exc_info=True) - if os.path.exists(temp_path): - try: os.remove(temp_path) - except OSError: pass - try: - if os.path.exists(uploads_dir) and not os.listdir(uploads_dir): - os.rmdir(uploads_dir) - except OSError as e: - logging.warning(f"Could not remove temporary upload directory {uploads_dir}: {e}") - - if new_photos_list: - old_photos = product_to_edit.get('photos', []) - if old_photos: - try: - api.delete_files( - repo_id=REPO_ID, paths_in_repo=[f"photos/{p}" for p in old_photos], - repo_type="dataset", token=HF_TOKEN_WRITE, - commit_message=f"Delete old photos for product {product_to_edit['name']}" - ) - except Exception as e: - logging.error(f"Error deleting old photos {old_photos} from HF: {e}", exc_info=True) - product_to_edit['photos'] = new_photos_list - flash("Фотографии товара успешно обновлены.", "success") - - products[product_index] = product_to_edit - data['products'] = products + for variant in product.get('variants', []): + if variant.get('photos'): + api.delete_files(repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE, paths_in_repo=[f"photos/{p}" for p in variant['photos']]) save_data(data) - logging.info(f"Product '{original_name}' (ID {product_id}) updated to '{product_to_edit['name']}'.") - flash(f"Товар '{product_to_edit['name']}' успешно обновлен.", 'success') - - elif action == 'delete_product': - product_id = request.form.get('product_id') - product_to_delete = next((p for p in products if p.get('id') == product_id), None) - - if product_to_delete: - product_name = product_to_delete.get('name', 'N/A') - products.remove(product_to_delete) - - photos_to_delete = product_to_delete.get('photos', []) - if photos_to_delete and HF_TOKEN_WRITE: - try: - api = HfApi() - api.delete_files( - repo_id=REPO_ID, paths_in_repo=[f"photos/{p}" for p in photos_to_delete], - repo_type="dataset", token=HF_TOKEN_WRITE, - commit_message=f"Delete photos for deleted product {product_name}" - ) - except Exception as e: - logging.error(f"Error deleting photos {photos_to_delete} from HF: {e}", exc_info=True) - - data['products'] = products - save_data(data) - logging.info(f"Product '{product_name}' (ID {product_id}) deleted.") - flash(f"Товар '{product_name}' удален.", 'success') - else: - flash(f"Ошибка удаления: неверный ID товара '{product_id}'.", 'error') - - else: - flash(f"Неизвестное действие: {action}", 'warning') - - return redirect(url_for('admin')) - - except Exception as e: - logging.error(f"Error processing admin action '{action}': {e}", exc_info=True) - flash(f"Произошла внутренняя ошибка при выполнении действия '{action}'.", 'error') - return redirect(url_for('admin')) - + flash(f"Товар '{product['name']}' удален.", 'success') + return redirect(url_for('admin')) + current_data = load_data() - display_products = sorted(current_data.get('products', []), key=lambda p: p.get('name', '').lower()) - display_categories = sorted(current_data.get('categories', [])) - display_employees = sorted(current_data.get('employees', [])) - return render_template_string( ADMIN_TEMPLATE, - products=display_products, - categories=display_categories, - employees=display_employees, - repo_id=REPO_ID, - currency_code=CURRENCY_CODE + products=sorted(current_data.get('products', []), key=lambda p: p.get('name', '').lower()), + categories=sorted(current_data.get('categories', [])), + seasons=sorted(current_data.get('seasons', [])), + employees=sorted(current_data.get('employees', [])), + settings=current_data.get('settings', {}), + repo_id=REPO_ID ) @app.route('/force_upload', methods=['POST']) def force_upload(): - logging.info("Forcing upload to Hugging Face...") - try: - upload_db_to_hf() - flash("Данные успешно загружены на Hugging Face.", 'success') - except Exception as e: - logging.error(f"Error during forced upload: {e}", exc_info=True) - flash(f"Ошибка при загрузке на Hugging Face: {e}", 'error') + upload_db_to_hf() + flash("Данные загружены на сервер.", 'success') return redirect(url_for('admin')) @app.route('/force_download', methods=['POST']) def force_download(): - logging.info("Forcing download from Hugging Face...") - try: - if download_db_from_hf(): - flash("Данные успешно скачаны с Hugging Face. Локальные файлы обновлены.", 'success') - load_data() - else: - flash("Не удалось скачать данные с Hugging Face после нескольких попыток.", 'error') - except Exception as e: - logging.error(f"Error during forced download: {e}", exc_info=True) - flash(f"Ошибка при скачивании с Hugging Face: {e}", 'error') + if download_db_from_hf(): + flash("Данные скачаны с сервера.", 'success') + else: + flash("Не удалось скачать данные.", 'error') return redirect(url_for('admin')) - if __name__ == '__main__': - logging.info("Application starting up. Performing initial data load/download...") + logging.info("Application starting...") download_db_from_hf() load_data() - logging.info("Initial data load complete.") if HF_TOKEN_WRITE: - backup_thread = threading.Thread(target=periodic_backup, daemon=True) - backup_thread.start() - logging.info("Periodic backup thread started.") + threading.Thread(target=periodic_backup, daemon=True).start() else: - logging.warning("Periodic backup will NOT run (HF_TOKEN for writing not set).") + logging.warning("Periodic backup disabled (HF_TOKEN_WRITE not set).") port = int(os.environ.get('PORT', 7860)) - logging.info(f"Starting Flask app on host 0.0.0.0 and port {port}") app.run(debug=False, host='0.0.0.0', port=port)