diff --git "a/app.py" "b/app.py" --- "a/app.py" +++ "b/app.py" @@ -1,5 +1,7 @@ -from flask import Flask, render_template_string, request, redirect, url_for, session, send_file +# --- START OF FILE Soola_Cosmetics.py --- + +from flask import Flask, render_template_string, request, redirect, url_for, session, send_from_directory import json import os import logging @@ -11,751 +13,433 @@ from huggingface_hub.utils import RepositoryNotFoundError from werkzeug.utils import secure_filename app = Flask(__name__) -app.secret_key = 'your_unique_secret_key_12345' # !!! CHANGE THIS TO A REAL SECRET KEY !!! -DATA_FILE = 'data_soola.json' -USERS_FILE = 'users_soola.json' +# Важно: Замените 'your_very_secure_secret_key_98765' на действительно случайный и секретный ключ +app.secret_key = os.environ.get('FLASK_SECRET_KEY', 'your_very_secure_secret_key_98765') + +# --- Настройки Магазина --- +SHOP_NAME = "Soola Cosmetics" +SHOP_ADDRESS = "Рынок Дордой, Джунхай, терминал, 38" +PRIMARY_CURRENCY = 'KGS' # Основная и единственная валюта - сом +PRIMARY_CURRENCY_SYMBOL = 'с' # Символ сома -# Список файлов для синхронизации (config.json убран) -SYNC_FILES = [DATA_FILE, USERS_FILE] +# --- Файлы Данных и Конфигурации --- +DATA_FILE = 'data_soola_cosmetics.json' +USERS_FILE = 'users_soola_cosmetics.json' +# CONFIG_FILE убран, так как курс больше не нужен -# Настройки Hugging Face -REPO_ID = "Kgshop/Soola" # Or change to your new repo ID like "YourUsername/SoolaCosmetics" -HF_TOKEN_WRITE = os.getenv("HF_TOKEN") -HF_TOKEN_READ = os.getenv("HF_TOKEN_READ") +# Список файлов для синхронизации с Hugging Face +SYNC_FILES = [DATA_FILE, USERS_FILE] # Убран CONFIG_FILE -# Адрес магазина (теперь один) -STORE_ADDRESS = "Рынок Дордой, Джунхай, терминал, 38" +# --- Настройки Hugging Face --- +# Убедитесь, что репозиторий соответствует вашему проекту +REPO_ID = "Kgshop/SoolaCosmetics" # Можно обновить имя репозитория, если хотите +HF_TOKEN_WRITE = os.getenv("HF_TOKEN") # Токен с правом записи +HF_TOKEN_READ = os.getenv("HF_TOKEN_READ") # Токен с правом чтения (может быть тот же) -# Настройка логирования -logging.basicConfig(level=logging.INFO) # Changed to INFO for less noise, set to DEBUG if needed +# --- Настройка Логирования --- +logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') -# --- Removed load_config, save_config, convert_price --- +# --- Функции Загрузки/Сохранения Данных --- def load_data(): - """Загрузка данных товаров и категорий.""" + """Загрузка данных о товарах и категориях.""" try: - # Attempt to download first to get the latest version - try: - download_db_from_hf(DATA_FILE) - except Exception as download_error: - logging.warning(f"Не удалось скачать {DATA_FILE} с HF, используется локальная версия (если есть): {download_error}") - - if not os.path.exists(DATA_FILE): - logging.warning(f"Локальный файл {DATA_FILE} не найден. Создание пустой структуры.") - return {'products': [], 'categories': []} - + # Попытка скачать актуальные данные перед чтением локального файла + download_db_from_hf() with open(DATA_FILE, 'r', encoding='utf-8') as file: data = json.load(file) logging.info(f"Данные успешно загружены из {DATA_FILE}") - if not isinstance(data, dict) or 'products' not in data or 'categories' not in data: - logging.warning(f"Структура файла {DATA_FILE} некорректна. Сброс к пустой структуре.") + # Проверка базовой структуры + if not isinstance(data, dict): + logging.warning(f"{DATA_FILE} имеет неверный формат (не словарь). Инициализация пустой структурой.") return {'products': [], 'categories': []} - # Ensure products and categories are lists - if not isinstance(data.get('products'), list): + if 'products' not in data: data['products'] = [] - if not isinstance(data.get('categories'), list): + if 'categories' not in data: data['categories'] = [] return data except FileNotFoundError: - logging.warning(f"Локальный файл {DATA_FILE} не найден. Создание пустой структуры.") - return {'products': [], 'categories': []} + logging.warning(f"Локальный файл {DATA_FILE} не найден. Попытка скачивания...") + try: + download_db_from_hf() + # Повторная попытка чтения после скачивания + with open(DATA_FILE, 'r', encoding='utf-8') as file: + data = json.load(file) + logging.info(f"Данные успешно загружены из {DATA_FILE} после скачивания.") + if not isinstance(data, dict): return {'products': [], 'categories': []} + if 'products' not in data: data['products'] = [] + if 'categories' not in data: data['categories'] = [] + return data + except FileNotFoundError: + logging.error(f"Файл {DATA_FILE} не найден даже после попытки скачивания. Создание пустой базы данных.") + return {'products': [], 'categories': []} + except RepositoryNotFoundError: + logging.error("Репозиторий Hugging Face не найден. Создание локальной пустой базы данных.") + return {'products': [], 'categories': []} + except Exception as e: + logging.error(f"Ошибка при загрузке данных после скачивания: {e}") + return {'products': [], 'categories': []} except json.JSONDecodeError: - logging.error(f"Ошибка: Невозможно декодировать JSON файл {DATA_FILE}.") - # Consider backing up the corrupted file here if needed + logging.error(f"Ошибка: Невозможно декодировать JSON из файла {DATA_FILE}. Возвращение пустых данных.") + # Попытка бэкапа испорченного файла + try: + corrupted_filename = f"{DATA_FILE}.corrupted_{datetime.now().strftime('%Y%m%d%H%M%S')}" + os.rename(DATA_FILE, corrupted_filename) + logging.info(f"Испорченный файл переименован в {corrupted_filename}") + except Exception as rename_e: + logging.error(f"Не удалось переименовать испорченный файл: {rename_e}") return {'products': [], 'categories': []} except Exception as e: - logging.error(f"Произошла ошибка при загрузке данных {DATA_FILE}: {e}") + logging.error(f"Непредвиденная ошибка при загрузке данных: {e}") return {'products': [], 'categories': []} def save_data(data): - """Сохранение данных товаров и категорий.""" + """Сохранение данных о товарах и категориях.""" try: with open(DATA_FILE, 'w', encoding='utf-8') as file: json.dump(data, file, ensure_ascii=False, indent=4) logging.info(f"Данные успешно сохранены в {DATA_FILE}") - upload_db_to_hf(DATA_FILE) # Upload specific file + # Запускаем загрузку на HF в отдельном потоке, чтобы не блокировать основной процесс + threading.Thread(target=upload_db_to_hf, args=([DATA_FILE],)).start() except Exception as e: logging.error(f"Ошибка при сохранении данных в {DATA_FILE}: {e}") - # Optionally re-raise or handle more gracefully - # raise + # Не пробрасываем исключение дальше, чтобы приложение продолжало работать, + # но ошибка залогирована. def load_users(): """Загрузка данных пользователей.""" try: - # Attempt to download first - try: - download_db_from_hf(USERS_FILE) - except Exception as download_error: - logging.warning(f"Не удалось скачать {USERS_FILE} с HF, используется локальная версия (если есть): {download_error}") - - if not os.path.exists(USERS_FILE): - logging.warning(f"Локальный файл {USERS_FILE} не найден. Создание пустого словаря.") - return {} - + # Попытка скачать актуальные данные перед чтением + download_db_from_hf([USERS_FILE]) # Скачиваем только файл пользователей with open(USERS_FILE, 'r', encoding='utf-8') as file: users = json.load(file) logging.info(f"Данные пользователей успешно загружены из {USERS_FILE}") - if not isinstance(users, dict): - logging.warning(f"Структура файла {USERS_FILE} некорректна. Сброс к пустому словарю.") - return {} - return users + return users if isinstance(users, dict) else {} except FileNotFoundError: - logging.warning(f"Локальный файл {USERS_FILE} не найден. Создание пустого словаря.") - return {} + logging.warning(f"Локальный файл {USERS_FILE} не найден. Попытка скачивания...") + try: + download_db_from_hf([USERS_FILE]) + with open(USERS_FILE, 'r', encoding='utf-8') as file: + users = json.load(file) + logging.info(f"Данные пользователей успешно загружены из {USERS_FILE} после скачивания.") + return users if isinstance(users, dict) else {} + except FileNotFoundError: + logging.error(f"Файл {USERS_FILE} не найден даже после скачивания. Создание пустого списка пользователей.") + return {} + except RepositoryNotFoundError: + logging.error("Репозиторий Hugging Face не найден при скачивании пользователей. Возвращение пустого списка.") + return {} + except Exception as e: + logging.error(f"Ошибка при загрузке пользователей после скачивания: {e}") + return {} except json.JSONDecodeError: - logging.error(f"Ошибка: Невозможно декодировать JSON файл {USERS_FILE}.") + logging.error(f"Ошибка декодирования JSON из {USERS_FILE}. Возвращение пустого списка.") + # Попытка бэкапа + try: + corrupted_filename = f"{USERS_FILE}.corrupted_{datetime.now().strftime('%Y%m%d%H%M%S')}" + os.rename(USERS_FILE, corrupted_filename) + logging.info(f"Испорченный файл пользователей переименован в {corrupted_filename}") + except Exception as rename_e: + logging.error(f"Не удалось переименовать испорченный файл пользователей: {rename_e}") return {} except Exception as e: - logging.error(f"Произошла ошибка при загрузке данных {USERS_FILE}: {e}") + logging.error(f"Непредвиденная ошибка при загрузке пользователей: {e}") return {} + def save_users(users): """Сохранение данных пользователей.""" try: with open(USERS_FILE, 'w', encoding='utf-8') as file: json.dump(users, file, ensure_ascii=False, indent=4) logging.info(f"Данные пользователей успешно сохранены в {USERS_FILE}") - upload_db_to_hf(USERS_FILE) # Upload specific file + # Запускаем загрузку на HF + threading.Thread(target=upload_db_to_hf, args=([USERS_FILE],)).start() except Exception as e: - logging.error(f"Ошибка при сохранении данных в {USERS_FILE}: {e}") - # Optionally re-raise or handle more gracefully - # raise + logging.error(f"Ошибка при сохранении пользователей в {USERS_FILE}: {e}") -def upload_db_to_hf(file_to_upload=None): - """Загрузка указанного файла или всех SYNC_FILES на Hugging Face.""" +# --- Функции Синхронизации с Hugging Face --- + +def upload_db_to_hf(files_to_sync=None): + """Загрузка указанных файлов (или всех SYNC_FILES) на Hugging Face.""" if not HF_TOKEN_WRITE: logging.warning("HF_TOKEN (write) не установлен. Загрузка на Hugging Face отключена.") return + if files_to_sync is None: + files_to_sync = SYNC_FILES + try: api = HfApi() - files_to_process = [file_to_upload] if file_to_upload else SYNC_FILES - - for file_name in files_to_process: + logging.info(f"Попытка загрузки файлов: {files_to_sync} в репозиторий {REPO_ID}") + for file_name in files_to_sync: if os.path.exists(file_name): - logging.info(f"Попытка загрузки {file_name} на Hugging Face...") - api.upload_file( - path_or_fileobj=file_name, - path_in_repo=file_name, - repo_id=REPO_ID, - repo_type="dataset", - token=HF_TOKEN_WRITE, - commit_message=f"Автоматическое резервное копирование файла {file_name} {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" - ) - logging.info(f"Резервная копия {file_name} успешно загружена на Hugging Face.") + try: + api.upload_file( + path_or_fileobj=file_name, + path_in_repo=file_name, # Путь в репозитории совпадает с именем файла + repo_id=REPO_ID, + repo_type="dataset", # Репозитории для данных обычно типа dataset + token=HF_TOKEN_WRITE, + commit_message=f"Auto-sync {file_name} {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" + ) + logging.info(f"Файл {file_name} успешно загружен на Hugging Face.") + except Exception as upload_exc: + logging.error(f"Ошибка при загрузке файла {file_name} на Hugging Face: {upload_exc}") else: - logging.warning(f"Файл {file_name} не найден для загрузки.") + logging.warning(f"Файл {file_name} не найден локально, пропуск загрузки.") except Exception as e: - logging.error(f"Ошибка при загрузке резервной копии на Hugging Face: {e}") + logging.error(f"Общая ошибка при инициализации или процессе загрузки на Hugging Face: {e}") + -def download_db_from_hf(file_to_download=None): - """Скачивание указанного файла или всех SYNC_FILES с Hugging Face.""" +def download_db_from_hf(files_to_sync=None): + """Скачивание указанных файлов (или всех SYNC_FILES) с Hugging Face.""" if not HF_TOKEN_READ: logging.warning("HF_TOKEN_READ не установлен. Скачивание с Hugging Face может быть недоступно для приватных репозиториев.") - # Allow public repo download attempt even without read token - # return # Uncomment this line if read token is strictly required + # Можно продолжить без токена для публичных репозиториев + # return # Раскомментируйте, если чтение без токена не нужно + + if files_to_sync is None: + files_to_sync = SYNC_FILES try: - api = HfApi() # Not strictly needed for download, but hf_hub_download uses underlying logic - files_to_process = [file_to_download] if file_to_download else SYNC_FILES - - for file_name in files_to_process: - logging.info(f"Попытка скачивания {file_name} из Hugging Face...") - hf_hub_download( - repo_id=REPO_ID, - filename=file_name, - repo_type="dataset", - token=HF_TOKEN_READ, # Pass None if not set, might work for public repos - local_dir=".", - local_dir_use_symlinks=False, - force_download=True # Ensure we get the latest version over local cache - ) - logging.info(f"Файл {file_name} успешно скачан из Hugging Face.") - except RepositoryNotFoundError as e: - logging.error(f"Репозиторий {REPO_ID} не найден на Hugging Face: {e}") - # Don't raise here, allow using local files if repo not found + api = HfApi() # Для скачивания токен не всегда обязателен, если репозиторий публичный + logging.info(f"Попытка скачивания файлов: {files_to_sync} из репозитория {REPO_ID}") + for file_name in files_to_sync: + try: + # Скачиваем файл в текущую директорию (.) + hf_hub_download( + repo_id=REPO_ID, + filename=file_name, # Имя файла в репозитории + repo_type="dataset", + token=HF_TOKEN_READ, # Передаем токен чтения, если он есть + local_dir=".", # Сохраняем в текущую папку + local_dir_use_symlinks=False # Важно для избежания проблем с символическими ссылками + ) + logging.info(f"Файл {file_name} успешно скачан из Hugging Face.") + except RepositoryNotFoundError: + logging.error(f"Репозиторий {REPO_ID} не найден на Hugging Face.") + raise # Пробрасываем ошибку, чтобы load_data мог ее обработать + except Exception as download_exc: + # Логир��ем ошибку для конкретного файла, но пытаемся скачать остальные + logging.error(f"Ошибка при скачивании файла {file_name} из Hugging Face: {download_exc}") + # Если файла нет в репо, hf_hub_download вызовет исключение (например, EntryNotFoundError) + # Не пробрасываем его дальше, чтобы не прерывать скачивание других файлов + # Но если файл критичен (как DATA_FILE), load_data обработает FileNotFoundError except Exception as e: - # Catch other potential download errors (network issues, file not found in repo, etc.) - logging.error(f"Ошибка при скачивании файла {file_name if file_name else 'files'} с Hugging Face: {e}") - # Don't raise, let the app try to use local files + logging.error(f"Общая ошибка при инициализации или процессе скачивания с Hugging Face: {e}") + # Не пробрасываем общую ошибку, чтобы приложение могло запуститься с локальными данными, если они есть def periodic_backup(): """Периодическая загрузка всех файлов на Hugging Face.""" + logging.info("Запуск потока периодического резервного копирования.") while True: - time.sleep(800) # Wait first - logging.info("Запуск периодического резервного копирования...") - # Load current data before backup? Maybe not necessary, just upload existing files. - upload_db_to_hf() # Upload all sync files + time.sleep(800) # Пауза в 800 секунд (примерно 13 минут) + logging.info("Выполнение планового резервного копирования...") + upload_db_to_hf() # Загружаем все файлы из SYNC_FILES -# Initial load on startup -load_data() -load_users() +# --- Маршруты Flask --- @app.route('/') def catalog(): + """Главная страница каталога товаров.""" data = load_data() products = data.get('products', []) categories = data.get('categories', []) is_authenticated = 'user' in session + # Валюта теперь фиксирована + current_currency = PRIMARY_CURRENCY + current_currency_symbol = PRIMARY_CURRENCY_SYMBOL - catalog_html = ''' + catalog_html = f'''
-{{ product['description'] }}
-{{ product.get('description', 'Нет описания') }}
{# Убрано ограничение длины, CSS теперь обрезает #} +Товар не найден.
", 404 - # Simplified HTML without currency conversion - detail_html = ''' -Категория: {{ product.get('category', 'Без категории') }}
- {% if is_authenticated %} -Цена: {{ product['price'] }} KGS
- {% else %} -Цена: Доступна после входа
- {% endif %} -Описание:
{{ product['description'] | replace('\\n', '
') | safe }}
Доступные цвета: {{ (product.get('colors') | join(', ')) if product.get('colors') else 'Стандартный' }}
-Категория: {{ product.get('category', 'Без категории') }}
+ + {{% if is_authenticated %}} ++ Цена: {{ "%.2f"|format(product['price']) }} {current_currency_symbol} {# Цена в сомах #} +
+ {{% else %}} +Цена: Доступна после входа
+ {{% endif %}} + +Описание:
+Доступные цвета/варианты: {{ product['colors']|join(', ') }}
+ {{% else %}} +Варианты: Стандартный
+ {{% endif %}}Вход выполнен успешно. Перенаправление...
+ ''' + return login_response_html else: - logging.warning(f"Неудачная попытка входа для пользователя {login}.") - return render_template_string(login_template, error="Неверный логин или пароль") - - # Keep using the variable for the template string - login_template = ''' - - - - - -- Нет аккаунта? Обратитесь к администратору для регистрации. -
+ logging.warning(f"Неудачная попытка входа для пользователя '{login_attempt}'.") + error_message = "Неверный логин или пароль." + # Возвращаем страницу входа снова с сообщением об ошибке + return render_template_string(LOGIN_TEMPLATE, error=error_message) + + # Отображаем страницу входа при GET запросе + return render_template_string(LOGIN_TEMPLATE, error=None) + +# Вынесем HTML шаблон для входа в константу для чистоты +LOGIN_TEMPLATE = ''' + + + + + +Выход... Перенаправление на главную страницу.
- ''') +Выход выполнен. Перенаправление...
+ ''' + return logout_response_html @app.route('/admin', methods=['GET', 'POST']) def admin(): - # Simple Admin Auth Check (replace with proper roles/permissions if needed) - if session.get('user') != 'admin': # Example: Only user 'admin' can access - # return "Доступ запрещен", 403 - pass # Allow any logged-in user for now, refine later + """Административная панель.""" + # Простая проверка "админа" - первый зарегистрированный пользователь? + # В реальном приложении нужна система ролей. + # users = load_users() + # if not session.get('user') or (users and session.get('user') != list(users.keys())[0]): + # return "Доступ запрещен", 403 + # Для простоты пока считаем, что любой залогиненный - админ :) + if 'user' not in session: + return redirect(url_for('login')) # Редирект на логин, если не авторизован data = load_data() products = data.get('products', []) categories = data.get('categories', []) users = load_users() - # kgs_to_usd removed + # Курс валют убран + # Обработка POST запросов для админ-панели if request.method == 'POST': action = request.form.get('action') + logging.info(f"Admin action requested: {action}") - try: # Wrap actions in try...except for better error handling + 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) - categories.sort() # Keep categories sorted - save_data(data) - logging.info(f"Категория '{category_name}' добавлена администратором {session.get('user')}.") - return redirect(url_for('admin')) + save_data(data) # Сохраняем изменения + logging.info(f"Добавлена новая категория: {category_name}") elif not category_name: - return "Ошибка: Название категории не может быть пустым.", 400 + logging.warning("Попытка добавить пустую категорию.") + # Можно добавить сообщение для пользователя через flash else: - return f"Ошибка: Категория '{category_name}' уже существует.", 400 + logging.warning(f"Попытка добавить существующую категорию: {category_name}") + # Можно добавить сообщение для пользователя elif action == 'delete_category': category_index_str = request.form.get('category_index') - if category_index_str is None: - return "Ошибка: Не указан индекс категории для удаления.", 400 - try: + if category_index_str is not None: category_index = int(category_index_str) if 0 <= category_index < len(categories): deleted_category = categories.pop(category_index) - # Update products using this category - updated_count = 0 + # Обновляем товары, которые были в этой категории for product in products: if product.get('category') == deleted_category: product['category'] = 'Без категории' - updated_count += 1 save_data(data) - logging.info(f"Категория '{deleted_category}' удалена администратором {session.get('user')}. Обновлено товаров: {updated_count}.") - return redirect(url_for('admin')) + logging.info(f"Удалена категория: {deleted_category}") else: - return "Ошибка: Неверный индекс категории.", 400 - except ValueError: - return "Ошибка: Неверный формат индекса категории.", 400 - - elif action == 'add_user': - login = request.form.get('login', '').strip() - password = request.form.get('password', '').strip() - # Implement password complexity rules here if needed - first_name = request.form.get('first_name', '').strip() - last_name = request.form.get('last_name', '').strip() - country = request.form.get('country', '').strip() - city = request.form.get('city', '').strip() - - if not login or not password: - return "Ошибка: Логин и пароль обязательны.", 400 - if login in users: - return f"Ошибка: Пользователь с логином '{login}' уже существует.", 400 - - # !!! IMPORTANT: Hash the password before saving !!! - # from werkzeug.security import generate_password_hash - # hashed_password = generate_password_hash(password) - users[login] = { - # 'password_hash': hashed_password, # Store the hash - 'password': password, # INSECURE: Replace with hashed password - 'first_name': first_name, - 'last_name': last_name, - 'country': country, - 'city': city, - # 'purchase_type': 'wholesale' # Implicitly wholesale - } - save_users(users) - logging.info(f"Пользователь '{login}' зарегистрирован администратором {session.get('user')}.") - return redirect(url_for('admin')) - - elif action == 'delete_user': - login_to_delete = request.form.get('login') - if not login_to_delete: - return "Ошибка: Не указан логин пользователя для удаления.", 400 - if login_to_delete == session.get('user'): # Prevent self-deletion? Or admin self-deletion? - return "Ошибка: Нельзя удалить самого себя.", 400 - if login_to_delete in users: - del users[login_to_delete] - save_users(users) - logging.info(f"Пользователь '{login_to_delete}' удален администратором {session.get('user')}.") - return redirect(url_for('admin')) - else: - return f"Ошибка: Пользователь '{login_to_delete}' не найден.", 404 + logging.error(f"Неверный индекс категории для удаления: {category_index}") + else: + logging.error("Индекс категории для удаления не указан.") - elif action == 'add' or action == 'edit': - # Common fields + elif action == 'add_product': name = request.form.get('name', '').strip() - price_str = request.form.get('price', '').replace(',', '.') + price_str = request.form.get('price', '0').replace(',', '.') description = request.form.get('description', '').strip() - category = request.form.get('category') - colors = [c.strip() for c in request.form.getlist('colors') if c.strip()] # Get list, strip whitespace, remove empty + category = request.form.get('category', 'Без категории') photos_files = request.files.getlist('photos') + # Получаем цвета, фильтруем пустые строки + colors = [c.strip() for c in request.form.getlist('colors') if c.strip()] + + if not name or not description: # Цена может быть 0 + logging.error("Ошибка добавления товара: не указано имя или описание.") + # Здесь хорошо бы вернуть ошибку пользователю (например, через flash messages) + return redirect(url_for('admin')) # Пока просто перезагружаем - if not name or not price_str or not description: - return "Ошибка: Название, цена и описание обязательны.", 400 try: - price = float(price_str) # Price is now KGS - if price < 0: - return "Ошибка: Цена не может быть отрицательной.", 400 + price = float(price_str) + if price < 0: price = 0 # Цена не может быть отрицательной except ValueError: - return "Ошибка: Неверный формат цены.", 400 - - if category not in categories and category != 'Без категории': - return f"Ошибка: Выбрана не существующая категория '{category}'.", 400 - if category == '': # Treat empty selection as 'Без категории' - category = 'Без категории' + logging.error(f"Неверный формат цены: {price_str}. Установлена цена 0.") + price = 0.0 - # Handle photos (common logic for add/edit) photos_list = [] - if action == 'edit': - index_str = request.form.get('index') - if index_str is None: return "Ошибка: Не указан индекс товара для редактирования.", 400 - try: - index = int(index_str) - if not (0 <= index < len(products)): return "Ошибка: Неверный индекс товара.", 400 - # Keep existing photos if no new ones are uploaded and it's an edit - photos_list = products[index].get('photos', []) - except ValueError: - return "Ошибка: Неверный формат индекса товара.", 400 - - # Check if new photos were actually uploaded - new_photos_uploaded = photos_files and any(f.filename for f in photos_files) - - if new_photos_uploaded: - # If new photos are uploaded for an existing product, replace old ones - if action == 'edit': - # TODO: Optionally delete old photos from Hugging Face? Complex. - photos_list = [] # Reset list for new photos - - # Upload new photos - if not HF_TOKEN_WRITE: - return "Ошибка: Токен HF_TOKEN (write) не настроен. Загрузка фото невозможна.", 500 - - uploads_dir = 'uploads_temp' # Temporary local storage + if photos_files: + uploads_dir = 'uploads_temp' # Временная папка для загрузок os.makedirs(uploads_dir, exist_ok=True) - api = HfApi() + api = HfApi() if HF_TOKEN_WRITE else None - for photo in photos_files[:10]: # Limit to 10 photos + for photo in photos_files: if photo and photo.filename: + # Генерируем безопасное и уникальное имя файла + base, ext = os.path.splitext(secure_filename(photo.filename)) + unique_filename = f"{base}_{int(time.time()*1000)}{ext}" + temp_path = os.path.join(uploads_dir, unique_filename) + try: - photo_filename = secure_filename(f"{datetime.now().strftime('%Y%m%d%H%M%S')}_{photo.filename}") - temp_path = os.path.join(uploads_dir, photo_filename) photo.save(temp_path) - - logging.info(f"Загрузка фото {photo_filename} на Hugging Face...") - api.upload_file( - path_or_fileobj=temp_path, - path_in_repo=f"photos/{photo_filename}", # Store in 'photos' directory - repo_id=REPO_ID, - repo_type="dataset", - token=HF_TOKEN_WRITE, - commit_message=f"Фото для товара '{name}' ({action}) админом {session.get('user')}" - ) - photos_list.append(photo_filename) - logging.info(f"Фото {photo_filename} успешно загружено.") - - # Clean up temporary file - if os.path.exists(temp_path): - os.remove(temp_path) - except Exception as upload_err: - logging.error(f"Ошибка загрузки фото {photo.filename}: {upload_err}") - # Clean up even on error + logging.info(f"Временный файл сохранен: {temp_path}") + + if api: + # Загружаем на Hugging Face + repo_photo_path = f"photos/{unique_filename}" + api.upload_file( + path_or_fileobj=temp_path, + path_in_repo=repo_photo_path, + repo_id=REPO_ID, + repo_type="dataset", + token=HF_TOKEN_WRITE, + commit_message=f"Add photo for product '{name}': {unique_filename}" + ) + photos_list.append(unique_filename) # Сохраняем только имя файла + logging.info(f"Фото загружено на HF: {repo_photo_path}") + else: + logging.warning("HF_TOKEN не настроен, фото не будет загружено на HF.") + # Если HF недоступен, фото не добавляем в список + except Exception as e: + logging.error(f"Ошибка обработки фото {unique_filename}: {e}") + finally: + # Удаляем временный файл после обработки if os.path.exists(temp_path): - try: os.remove(temp_path) - except: pass - # Optionally: return error to user or just log it - return f"Ошибка при загрузке фото {photo.filename}. Проверьте логи.", 500 - elif photo and not photo.filename: - pass # Ignore empty file inputs - # Clean up temp dir if empty? Maybe not necessary. - - # Create or update product dictionary - product_data = { + try: + os.remove(temp_path) + logging.info(f"Временный файл удален: {temp_path}") + except OSError as remove_e: + logging.error(f"Не удалось удалить временный файл {temp_path}: {remove_e}") + else: + logging.debug("Пропущен пустой файл в форме загрузки фото.") + + + new_product = { + 'id': f"prod_{int(time.time()*1000)}", # Простой уникальный ID 'name': name, - 'price': price, # KGS price + 'price': price, # Цена теперь всегда в KGS 'description': description, - 'category': category, - 'photos': photos_list, # Updated or new list - 'colors': colors if colors else [] # Ensure it's a list, even if empty + 'category': category if category in categories else 'Без категории', + 'photos': photos_list[:10], # Ограничение на количество фото + 'colors': colors if colors else [] # Пустой список, если цвета не указаны } + products.append(new_product) + save_data(data) + logging.info(f"Добавлен новый товар: {name}") - if action == 'add': - products.append(product_data) - logging.info(f"Товар '{name}' добавлен администратором {session.get('user')}.") - else: # action == 'edit' - # Preserve other potential keys if they exist - products[index].update(product_data) - logging.info(f"Товар '{name}' (индекс {index}) отредактирован администратором {session.get('user')}.") - save_data(data) - return redirect(url_for('admin')) + elif action == 'edit_product': + index_str = request.form.get('index') + if index_str is None: + logging.error("Ошибка редактирования: индекс товара не указан.") + return redirect(url_for('admin')) + + index = int(index_str) + if 0 <= index < len(products): + product_to_edit = products[index] + original_name = product_to_edit.get('name', 'Без имени') + logging.info(f"Редактирование товара '{original_name}' (индекс {index})") + + # Обновляем поля + product_to_edit['name'] = request.form.get('name', original_name).strip() + price_str = request.form.get('price', str(product_to_edit.get('price', 0))).replace(',', '.') + product_to_edit['description'] = request.form.get('description', product_to_edit.get('description', '')).strip() + product_to_edit['category'] = request.form.get('category', product_to_edit.get('category', 'Без категории')) + if product_to_edit['category'] not in categories and product_to_edit['category'] != 'Без категории': + product_to_edit['category'] = 'Без категории' + + # Обработка цены + try: + price = float(price_str) + product_to_edit['price'] = price if price >= 0 else 0 + except ValueError: + logging.warning(f"Неверный формат цены при редактировании товара '{product_to_edit['name']}'. Цена не изменена.") + # Оставляем старую цену product_to_edit['price'] + + # Обработка цветов + product_to_edit['colors'] = [c.strip() for c in request.form.getlist('colors') if c.strip()] + + # Обработка фото (если загружены новые - заменяем старые) + photos_files = request.files.getlist('photos') + if photos_files and any(f.filename for f in photos_files): + # Удаляем старые фото с HF (если нужно и возможно) - ЭТО СЛОЖНО, ПРОПУСТИМ ПОКА + # logging.info(f"Удаление старых фото для {product_to_edit['name']} (если они есть)...") + # for old_photo in product_to_edit.get('photos', []): + # # Нужен HfApi и delete_file, обработка ошибок + # pass + + new_photos_list = [] + uploads_dir = 'uploads_temp' + os.makedirs(uploads_dir, exist_ok=True) + api = HfApi() if HF_TOKEN_WRITE else None + + for photo in photos_files: + if photo and photo.filename: + base, ext = os.path.splitext(secure_filename(photo.filename)) + unique_filename = f"{base}_{int(time.time()*1000)}{ext}" + temp_path = os.path.join(uploads_dir, unique_filename) + try: + photo.save(temp_path) + if api: + repo_photo_path = f"photos/{unique_filename}" + api.upload_file( + path_or_fileobj=temp_path, + path_in_repo=repo_photo_path, + repo_id=REPO_ID, + repo_type="dataset", + token=HF_TOKEN_WRITE, + commit_message=f"Update photo for product '{product_to_edit['name']}': {unique_filename}" + ) + new_photos_list.append(unique_filename) + logging.info(f"Новое фото загружено на HF: {repo_photo_path}") + else: + logging.warning("HF_TOKEN не настроен, новое фото не будет загружено на HF.") + + except Exception as e: + logging.error(f"Ошибка обработки нового фото {unique_filename} при редактировании: {e}") + finally: + if os.path.exists(temp_path): + try: + os.remove(temp_path) + except OSError as remove_e: + logging.error(f"Не удалось удалить временный файл {temp_path}: {remove_e}") + # Заменяем список фото, если были загружены новые + if new_photos_list: + product_to_edit['photos'] = new_photos_list[:10] # Ограничение + logging.info(f"Список фото для товара '{product_to_edit['name']}' обновлен.") + # Если новые фото не загружались, список 'photos' остается прежним + + + save_data(data) + logging.info(f"Товар '{product_to_edit['name']}' успешно обновлен.") + else: + logging.error(f"Ошибка редактирования: неверный индекс товара {index}.") - elif action == 'delete': + elif action == 'delete_product': index_str = request.form.get('index') - if index_str is None: return "Ошибка: Не указан индекс товара для удаления.", 400 - try: - index = int(index_str) - if 0 <= index < len(products): + if index_str is not None: + index = int(index_str) + if 0 <= index < len(products): deleted_product = products.pop(index) - # TODO: Optionally delete associated photos from Hugging Face? Complex. + # Удаление фото с HF - СЛОЖНО, ПРОПУСКАЕМ save_data(data) - logging.info(f"Товар '{deleted_product.get('name', 'Unknown')}' (индекс {index}) удален администратором {session.get('user')}.") - return redirect(url_for('admin')) - else: - return "Ошибка: Неверный индекс товара.", 400 - except ValueError: - return "Ошибка: Неверный формат индекса товара.", 400 + logging.info(f"Удален товар: {deleted_product.get('name', 'Без имени')}") + else: + logging.error(f"Ошибка удаления: неверный индекс товара {index}.") + else: + logging.error("Индекс товара для удаления не указан.") + + # --- Управление пользователями --- + elif action == 'add_user': + login = request.form.get('login', '').strip() + password = request.form.get('password', '').strip() + first_name = request.form.get('first_name', '').strip() + last_name = request.form.get('last_name', '').strip() + country = request.form.get('country', '').strip() + city = request.form.get('city', '').strip() + + if not login or not password: + logging.error("Ошибка добавления пользователя: логин и пароль обязательны.") + # Нужен flash message + elif login in users: + logging.warning(f"Пользователь с логином '{login}' уже существует.") + # Нужен flash message + else: + users[login] = { + 'password': password, # В реальном приложении пароль нужно хешировать! + 'first_name': first_name, + 'last_name': last_name, + 'country': country, + 'city': city, + # 'purchase_type': 'wholesale' # Тип покупки больше не нужен + 'created_at': datetime.now().strftime('%Y-%m-%d %H:%M:%S') # Дата создания + } + save_users(users) + logging.info(f"Добавлен новый пользователь: {login}") + + elif action == 'delete_user': + login_to_delete = request.form.get('login') + if login_to_delete and login_to_delete in users: + # Добавить проверку, чтобы админ не удалил сам себя? + # if login_to_delete == session.get('user'): + # logging.warning("Попытка удалить текущего пользователя.") + # # Flash message + # else: + del users[login_to_delete] + save_users(users) + logging.info(f"Удален пользователь: {login_to_delete}") + elif not login_to_delete: + logging.error("Логин пользователя для удаления не указан.") + else: + logging.error(f"Пользователь '{login_to_delete}' для удаления не найден.") - # --- Removed set_exchange_rate action --- + # Удаление секции с курсом валют + # elif action == 'set_exchange_rate': ... else: - return "Ошибка: Неизвестное действие.", 400 + logging.warning(f"Неизвестное действие в админ-панели: {action}") + + # После любого действия перенаправляем обратно на админку + return redirect(url_for('admin')) except Exception as e: - logging.error(f"Ошибка в админ-панели (действие: {action}): {e}", exc_info=True) - return f"Внутренняя ошибка сервера при выполнении действия '{action}'. См. логи.", 500 + logging.exception(f"Критическая ошибка при обработке действия '{action}' в админ-панели:") + # Можно показать страницу с ошибкой или просто редирект + return redirect(url_for('admin')) - # --- Admin HTML Template --- - admin_html = ''' + # Отображение админ-панели при GET запросе + admin_html = f''' -{{ category }}
+ +Категорий пока нет.
+ {{% endif %}}Категорий пока нет.
- {% endif %} + +Синхронизация отправляет локальные файлы базы данных на сервер Hugging Face. Скачивание заменяет локальные файлы версией с сервера.
- - -Нет фото
- {% endif %} -Категория: {{ product.get('category', 'Без категории') }}
-Цена: {{ product['price'] }} KGS
-Описание: {{ product['description'][:100] }}{% if product['description']|length > 100 %}...{% endif %}
-Цвета: {{ (product.get('colors') | join(', ')) if product.get('colors') else 'N/A' }}
- -ID: {{ product.get('id', 'N/A') }}
+Категория: {{ product.get('category', 'Без категории') }}
+Цена: {{ "%.2f"|format(product['price']) }} {PRIMARY_CURRENCY_SYMBOL}
{# Цена в KGS #} +Описание: {{ product['description'][:100] }}{% if product['description']|length > 100 %}...{% endif %}
{# Краткое описание #} +Варианты: {{ (product.get('colors')|join(', ')) if product.get('colors') else 'Стандартный' }}
+ {{% if product.get('photos') %}} +Товаров пока нет.
- {% endfor %} + {{% endif %}} + +Товаров пока нет.
+ {{% endif %}} +Пользователей пока нет (кроме, возможно, администратора).
- {% endfor %} -Логин: {{ login }}
+Имя: {{ user_data.get('first_name', '-') }} {{ user_data.get('last_name', '') }}
+Регион: {{ user_data.get('country', '-') }}, {{ user_data.get('city', '-') }}
+Дата создания: {{ user_data.get('created_at', 'Неизвестно') }}
+ {# Пароль не отображаем #} +Пользователей пока нет.
+ {{% endif %}} +