Update app.py
Browse files
app.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
|
|
| 1 |
|
| 2 |
-
from flask import Flask, render_template_string, request, redirect, url_for, session, send_file, flash
|
| 3 |
import json
|
| 4 |
import os
|
| 5 |
import logging
|
|
@@ -10,7 +11,8 @@ from huggingface_hub import HfApi, hf_hub_download
|
|
| 10 |
from huggingface_hub.utils import RepositoryNotFoundError, HfHubHTTPError
|
| 11 |
from werkzeug.utils import secure_filename
|
| 12 |
from dotenv import load_dotenv
|
| 13 |
-
import requests
|
|
|
|
| 14 |
|
| 15 |
load_dotenv()
|
| 16 |
|
|
@@ -31,14 +33,13 @@ CURRENCY_CODE = 'KGS'
|
|
| 31 |
CURRENCY_NAME = 'Кыргызский сом (с)'
|
| 32 |
|
| 33 |
DOWNLOAD_RETRIES = 3
|
| 34 |
-
DOWNLOAD_DELAY = 5
|
| 35 |
|
| 36 |
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
| 37 |
|
| 38 |
def download_db_from_hf(specific_file=None, retries=DOWNLOAD_RETRIES, delay=DOWNLOAD_DELAY):
|
| 39 |
-
if not HF_TOKEN_READ and not HF_TOKEN_WRITE:
|
| 40 |
logging.warning("HF_TOKEN_READ/HF_TOKEN_WRITE not set. Download might fail for private repos.")
|
| 41 |
-
# Continue attempt without token for public repos
|
| 42 |
|
| 43 |
token_to_use = HF_TOKEN_READ if HF_TOKEN_READ else HF_TOKEN_WRITE
|
| 44 |
|
|
@@ -59,22 +60,22 @@ def download_db_from_hf(specific_file=None, retries=DOWNLOAD_RETRIES, delay=DOWN
|
|
| 59 |
local_dir=".",
|
| 60 |
local_dir_use_symlinks=False,
|
| 61 |
force_download=True,
|
| 62 |
-
resume_download=False
|
| 63 |
)
|
| 64 |
logging.info(f"Successfully downloaded {file_name} to {local_path}.")
|
| 65 |
success = True
|
| 66 |
-
break
|
| 67 |
except RepositoryNotFoundError:
|
| 68 |
logging.error(f"Repository {REPO_ID} not found. Download cancelled for all files.")
|
| 69 |
-
return False
|
| 70 |
except HfHubHTTPError as e:
|
| 71 |
if e.response.status_code == 404:
|
| 72 |
logging.warning(f"File {file_name} not found in repo {REPO_ID} (404). Skipping this file.")
|
| 73 |
-
success = False
|
| 74 |
-
break
|
| 75 |
else:
|
| 76 |
logging.error(f"HTTP error downloading {file_name} (Attempt {attempt + 1}): {e}. Retrying in {delay}s...")
|
| 77 |
-
except requests.exceptions.RequestException as e:
|
| 78 |
logging.error(f"Network error downloading {file_name} (Attempt {attempt + 1}): {e}. Retrying in {delay}s...")
|
| 79 |
except Exception as e:
|
| 80 |
logging.error(f"Unexpected error downloading {file_name} (Attempt {attempt + 1}): {e}. Retrying in {delay}s...", exc_info=True)
|
|
@@ -84,7 +85,7 @@ def download_db_from_hf(specific_file=None, retries=DOWNLOAD_RETRIES, delay=DOWN
|
|
| 84 |
|
| 85 |
if not success:
|
| 86 |
logging.error(f"Failed to download {file_name} after {retries + 1} attempts.")
|
| 87 |
-
all_successful = False
|
| 88 |
|
| 89 |
logging.info(f"Download process finished. Overall success: {all_successful}")
|
| 90 |
return all_successful
|
|
@@ -97,7 +98,7 @@ def load_data():
|
|
| 97 |
logging.info(f"Local data loaded successfully from {DATA_FILE}")
|
| 98 |
if not isinstance(data, dict):
|
| 99 |
logging.warning(f"Local {DATA_FILE} is not a dictionary. Attempting download.")
|
| 100 |
-
raise FileNotFoundError
|
| 101 |
if 'products' not in data: data['products'] = []
|
| 102 |
if 'categories' not in data: data['categories'] = []
|
| 103 |
return data
|
|
@@ -106,10 +107,8 @@ def load_data():
|
|
| 106 |
except json.JSONDecodeError:
|
| 107 |
logging.error(f"Error decoding JSON in local {DATA_FILE}. File might be corrupt. Attempting download.")
|
| 108 |
|
| 109 |
-
# Proceed to download only if local loading failed
|
| 110 |
if download_db_from_hf(specific_file=DATA_FILE):
|
| 111 |
try:
|
| 112 |
-
# Try loading the newly downloaded file
|
| 113 |
with open(DATA_FILE, 'r', encoding='utf-8') as file:
|
| 114 |
data = json.load(file)
|
| 115 |
logging.info(f"Data loaded successfully from {DATA_FILE} after download.")
|
|
@@ -130,11 +129,10 @@ def load_data():
|
|
| 130 |
return default_data
|
| 131 |
else:
|
| 132 |
logging.error(f"Failed to download {DATA_FILE} from HF after retries. Using empty default data structure.")
|
| 133 |
-
return default_data
|
| 134 |
|
| 135 |
def save_data(data):
|
| 136 |
try:
|
| 137 |
-
# Ensure the structure is valid before saving
|
| 138 |
if not isinstance(data, dict):
|
| 139 |
logging.error("Attempted to save invalid data structure (not a dict). Aborting save.")
|
| 140 |
return
|
|
@@ -160,10 +158,8 @@ def load_users():
|
|
| 160 |
except json.JSONDecodeError:
|
| 161 |
logging.error(f"Error decoding JSON in local {USERS_FILE}. File might be corrupt. Attempting download.")
|
| 162 |
|
| 163 |
-
# Proceed to download only if local loading failed
|
| 164 |
if download_db_from_hf(specific_file=USERS_FILE):
|
| 165 |
try:
|
| 166 |
-
# Try loading the newly downloaded file
|
| 167 |
with open(USERS_FILE, 'r', encoding='utf-8') as file:
|
| 168 |
users = json.load(file)
|
| 169 |
logging.info(f"Users loaded successfully from {USERS_FILE} after download.")
|
|
@@ -179,7 +175,7 @@ def load_users():
|
|
| 179 |
return default_users
|
| 180 |
else:
|
| 181 |
logging.error(f"Failed to download {USERS_FILE} from HF after retries. Using empty default user structure.")
|
| 182 |
-
return default_users
|
| 183 |
|
| 184 |
def save_users(users):
|
| 185 |
try:
|
|
@@ -733,46 +729,44 @@ def catalog():
|
|
| 733 |
alert("Корзина пуста! Добавьте товары перед заказом.");
|
| 734 |
return;
|
| 735 |
}
|
| 736 |
-
let total = 0;
|
| 737 |
-
let orderText = "🛍️ *Новый Заказ от Soola Cosmetics* 🛍️%0A";
|
| 738 |
-
orderText += "----------------------------------------%0A";
|
| 739 |
-
orderText += "*Детали заказа:*%0A";
|
| 740 |
-
orderText += "----------------------------------------%0A";
|
| 741 |
-
cart.forEach((item, index) => {
|
| 742 |
-
const itemTotal = item.price * item.quantity;
|
| 743 |
-
total += itemTotal;
|
| 744 |
-
const colorText = item.color !== 'N/A' ? ` (${item.color})` : '';
|
| 745 |
-
orderText += `${index + 1}. *${item.name}*${colorText}%0A`;
|
| 746 |
-
orderText += ` Кол-во: ${item.quantity}%0A`;
|
| 747 |
-
orderText += ` Цена: ${item.price.toFixed(2)} ${currencyCode}%0A`;
|
| 748 |
-
orderText += ` *Сумма: ${itemTotal.toFixed(2)} ${currencyCode}*%0A%0A`;
|
| 749 |
-
});
|
| 750 |
-
orderText += "----------------------------------------%0A";
|
| 751 |
-
orderText += `*ИТОГО: ${total.toFixed(2)} ${currencyCode}*%0A`;
|
| 752 |
-
orderText += "----------------------------------------%0A";
|
| 753 |
-
|
| 754 |
-
if (userInfo && userInfo.login) {
|
| 755 |
-
orderText += "*Данные клиента:*%0A";
|
| 756 |
-
orderText += `Имя: ${userInfo.first_name || ''} ${userInfo.last_name || ''}%0A`;
|
| 757 |
-
orderText += `Логин: ${userInfo.login}%0A`;
|
| 758 |
-
if (userInfo.phone) {
|
| 759 |
-
orderText += `Телефон: ${userInfo.phone}%0A`;
|
| 760 |
-
}
|
| 761 |
-
orderText += `Страна: ${userInfo.country || 'Не указана'}%0A`;
|
| 762 |
-
orderText += `Город: ${userInfo.city || 'Не указан'}%0A`;
|
| 763 |
-
} else {
|
| 764 |
-
orderText += "*Клиент не авторизован*%0A";
|
| 765 |
-
}
|
| 766 |
-
orderText += "----------------------------------------%0A";
|
| 767 |
|
| 768 |
-
|
| 769 |
-
|
| 770 |
-
|
| 771 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 772 |
|
| 773 |
-
|
| 774 |
-
|
| 775 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 776 |
}
|
| 777 |
|
| 778 |
|
|
@@ -951,6 +945,137 @@ def product_detail(index):
|
|
| 951 |
currency_code=CURRENCY_CODE
|
| 952 |
)
|
| 953 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 954 |
LOGIN_TEMPLATE = '''
|
| 955 |
<!DOCTYPE html>
|
| 956 |
<html lang="ru">
|
|
@@ -1464,7 +1589,8 @@ ADMIN_TEMPLATE = '''
|
|
| 1464 |
if (group) {
|
| 1465 |
const container = group.parentNode;
|
| 1466 |
group.remove();
|
| 1467 |
-
if
|
|
|
|
| 1468 |
const placeholderGroup = document.createElement('div');
|
| 1469 |
placeholderGroup.className = 'color-input-group';
|
| 1470 |
placeholderGroup.innerHTML = `
|
|
@@ -1484,10 +1610,8 @@ ADMIN_TEMPLATE = '''
|
|
| 1484 |
|
| 1485 |
@app.route('/admin', methods=['GET', 'POST'])
|
| 1486 |
def admin():
|
| 1487 |
-
# Load data and users *before* processing POST to have the current state
|
| 1488 |
data = load_data()
|
| 1489 |
users = load_users()
|
| 1490 |
-
# Ensure products and categories are lists/dicts even if load failed
|
| 1491 |
products = data.get('products', [])
|
| 1492 |
categories = data.get('categories', [])
|
| 1493 |
|
|
@@ -1501,7 +1625,7 @@ def admin():
|
|
| 1501 |
if category_name and category_name not in categories:
|
| 1502 |
categories.append(category_name)
|
| 1503 |
categories.sort()
|
| 1504 |
-
data['categories'] = categories
|
| 1505 |
save_data(data)
|
| 1506 |
logging.info(f"Category '{category_name}' added.")
|
| 1507 |
flash(f"Категория '{category_name}' ус��ешно добавлена.", 'success')
|
|
@@ -1521,8 +1645,8 @@ def admin():
|
|
| 1521 |
if product.get('category') == category_to_delete:
|
| 1522 |
product['category'] = 'Без категории'
|
| 1523 |
updated_count += 1
|
| 1524 |
-
data['categories'] = categories
|
| 1525 |
-
data['products'] = products
|
| 1526 |
save_data(data)
|
| 1527 |
logging.info(f"Category '{category_to_delete}' deleted. Updated products: {updated_count}.")
|
| 1528 |
flash(f"Категория '{category_to_delete}' удалена.", 'success')
|
|
@@ -1566,7 +1690,6 @@ def admin():
|
|
| 1566 |
if photo and photo.filename:
|
| 1567 |
try:
|
| 1568 |
ext = os.path.splitext(photo.filename)[1].lower()
|
| 1569 |
-
# Basic check for common image extensions
|
| 1570 |
if ext not in ['.jpg', '.jpeg', '.png', '.gif', '.webp']:
|
| 1571 |
logging.warning(f"Skipping non-image file upload: {photo.filename}")
|
| 1572 |
flash(f"Файл {photo.filename} не является изображением и был пропущен.", "warning")
|
|
@@ -1591,14 +1714,12 @@ def admin():
|
|
| 1591 |
except Exception as e:
|
| 1592 |
logging.error(f"Error uploading photo {photo.filename} to HF: {e}", exc_info=True)
|
| 1593 |
flash(f"Ошибка при загрузке фото {photo.filename}.", 'error')
|
| 1594 |
-
# Optionally remove temp file if it exists after error
|
| 1595 |
if os.path.exists(temp_path):
|
| 1596 |
try: os.remove(temp_path)
|
| 1597 |
except OSError: pass
|
| 1598 |
elif photo and not photo.filename:
|
| 1599 |
logging.warning("Received an empty photo file object when adding product.")
|
| 1600 |
try:
|
| 1601 |
-
# Check if directory exists and is empty before removing
|
| 1602 |
if os.path.exists(uploads_dir) and not os.listdir(uploads_dir):
|
| 1603 |
os.rmdir(uploads_dir)
|
| 1604 |
except OSError as e:
|
|
@@ -1611,7 +1732,7 @@ def admin():
|
|
| 1611 |
'in_stock': in_stock, 'is_top': is_top
|
| 1612 |
}
|
| 1613 |
products.append(new_product)
|
| 1614 |
-
data['products'] = products
|
| 1615 |
save_data(data)
|
| 1616 |
logging.info(f"Product '{name}' added.")
|
| 1617 |
flash(f"Товар '{name}' успешно добавлен.", 'success')
|
|
@@ -1624,7 +1745,6 @@ def admin():
|
|
| 1624 |
|
| 1625 |
try:
|
| 1626 |
index = int(index_str)
|
| 1627 |
-
# Use the 'products' list loaded at the start of the function
|
| 1628 |
if not (0 <= index < len(products)):
|
| 1629 |
raise IndexError("Product index out of range")
|
| 1630 |
product_to_edit = products[index]
|
|
@@ -1719,9 +1839,8 @@ def admin():
|
|
| 1719 |
elif uploaded_count == 0 and any(f.filename for f in photos_files):
|
| 1720 |
flash("Не удалось загрузить новые фотографии.", "error")
|
| 1721 |
|
| 1722 |
-
# Update the product in the main list
|
| 1723 |
products[index] = product_to_edit
|
| 1724 |
-
data['products'] = products
|
| 1725 |
save_data(data)
|
| 1726 |
logging.info(f"Product '{original_name}' (index {index}) updated to '{product_to_edit['name']}'.")
|
| 1727 |
flash(f"Товар '{product_to_edit['name']}' успешно обновлен.", 'success')
|
|
@@ -1733,7 +1852,6 @@ def admin():
|
|
| 1733 |
return redirect(url_for('admin'))
|
| 1734 |
try:
|
| 1735 |
index = int(index_str)
|
| 1736 |
-
# Use the 'products' list loaded at the start of the function
|
| 1737 |
if not (0 <= index < len(products)): raise IndexError("Product index out of range")
|
| 1738 |
deleted_product = products.pop(index)
|
| 1739 |
product_name = deleted_product.get('name', 'N/A')
|
|
@@ -1755,7 +1873,7 @@ def admin():
|
|
| 1755 |
logging.error(f"Error deleting photos {photos_to_delete} for product '{product_name}' from HF: {e}", exc_info=True)
|
| 1756 |
flash(f"Не удалось удалить фото для товара '{product_name}' с сервера.", "warning")
|
| 1757 |
|
| 1758 |
-
data['products'] = products
|
| 1759 |
save_data(data)
|
| 1760 |
logging.info(f"Product '{product_name}' (original index {index}) deleted.")
|
| 1761 |
flash(f"Товар '{product_name}' удален.", 'success')
|
|
@@ -1811,9 +1929,6 @@ def admin():
|
|
| 1811 |
flash(f"Произошла внутренняя ошибка при выполнении действия '{action}'. Подробности в логе сервера.", 'error')
|
| 1812 |
return redirect(url_for('admin'))
|
| 1813 |
|
| 1814 |
-
# GET request or after POST redirect
|
| 1815 |
-
# Reload data and users again in case they were modified by POST and saved
|
| 1816 |
-
# This ensures the template always shows the latest state after an action
|
| 1817 |
current_data = load_data()
|
| 1818 |
current_users = load_users()
|
| 1819 |
display_products = current_data.get('products', [])
|
|
@@ -1854,7 +1969,6 @@ def force_download():
|
|
| 1854 |
return redirect(url_for('admin'))
|
| 1855 |
|
| 1856 |
if __name__ == '__main__':
|
| 1857 |
-
# Initial load on startup
|
| 1858 |
load_data()
|
| 1859 |
load_users()
|
| 1860 |
|
|
@@ -1867,7 +1981,6 @@ if __name__ == '__main__':
|
|
| 1867 |
|
| 1868 |
port = int(os.environ.get('PORT', 7860))
|
| 1869 |
logging.info(f"Starting Flask app on host 0.0.0.0 and port {port}")
|
| 1870 |
-
# Use waitress or gunicorn in production instead of development server
|
| 1871 |
-
# For simplicity, using Flask's dev server here
|
| 1872 |
app.run(debug=False, host='0.0.0.0', port=port)
|
| 1873 |
|
|
|
|
|
|
| 1 |
+
# --- START OF FILE app.py ---
|
| 2 |
|
| 3 |
+
from flask import Flask, render_template_string, request, redirect, url_for, session, send_file, flash, jsonify
|
| 4 |
import json
|
| 5 |
import os
|
| 6 |
import logging
|
|
|
|
| 11 |
from huggingface_hub.utils import RepositoryNotFoundError, HfHubHTTPError
|
| 12 |
from werkzeug.utils import secure_filename
|
| 13 |
from dotenv import load_dotenv
|
| 14 |
+
import requests
|
| 15 |
+
import base64 # Added for encoding order data
|
| 16 |
|
| 17 |
load_dotenv()
|
| 18 |
|
|
|
|
| 33 |
CURRENCY_NAME = 'Кыргызский сом (с)'
|
| 34 |
|
| 35 |
DOWNLOAD_RETRIES = 3
|
| 36 |
+
DOWNLOAD_DELAY = 5
|
| 37 |
|
| 38 |
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
| 39 |
|
| 40 |
def download_db_from_hf(specific_file=None, retries=DOWNLOAD_RETRIES, delay=DOWNLOAD_DELAY):
|
| 41 |
+
if not HF_TOKEN_READ and not HF_TOKEN_WRITE:
|
| 42 |
logging.warning("HF_TOKEN_READ/HF_TOKEN_WRITE not set. Download might fail for private repos.")
|
|
|
|
| 43 |
|
| 44 |
token_to_use = HF_TOKEN_READ if HF_TOKEN_READ else HF_TOKEN_WRITE
|
| 45 |
|
|
|
|
| 60 |
local_dir=".",
|
| 61 |
local_dir_use_symlinks=False,
|
| 62 |
force_download=True,
|
| 63 |
+
resume_download=False
|
| 64 |
)
|
| 65 |
logging.info(f"Successfully downloaded {file_name} to {local_path}.")
|
| 66 |
success = True
|
| 67 |
+
break
|
| 68 |
except RepositoryNotFoundError:
|
| 69 |
logging.error(f"Repository {REPO_ID} not found. Download cancelled for all files.")
|
| 70 |
+
return False
|
| 71 |
except HfHubHTTPError as e:
|
| 72 |
if e.response.status_code == 404:
|
| 73 |
logging.warning(f"File {file_name} not found in repo {REPO_ID} (404). Skipping this file.")
|
| 74 |
+
success = False
|
| 75 |
+
break
|
| 76 |
else:
|
| 77 |
logging.error(f"HTTP error downloading {file_name} (Attempt {attempt + 1}): {e}. Retrying in {delay}s...")
|
| 78 |
+
except requests.exceptions.RequestException as e:
|
| 79 |
logging.error(f"Network error downloading {file_name} (Attempt {attempt + 1}): {e}. Retrying in {delay}s...")
|
| 80 |
except Exception as e:
|
| 81 |
logging.error(f"Unexpected error downloading {file_name} (Attempt {attempt + 1}): {e}. Retrying in {delay}s...", exc_info=True)
|
|
|
|
| 85 |
|
| 86 |
if not success:
|
| 87 |
logging.error(f"Failed to download {file_name} after {retries + 1} attempts.")
|
| 88 |
+
all_successful = False
|
| 89 |
|
| 90 |
logging.info(f"Download process finished. Overall success: {all_successful}")
|
| 91 |
return all_successful
|
|
|
|
| 98 |
logging.info(f"Local data loaded successfully from {DATA_FILE}")
|
| 99 |
if not isinstance(data, dict):
|
| 100 |
logging.warning(f"Local {DATA_FILE} is not a dictionary. Attempting download.")
|
| 101 |
+
raise FileNotFoundError
|
| 102 |
if 'products' not in data: data['products'] = []
|
| 103 |
if 'categories' not in data: data['categories'] = []
|
| 104 |
return data
|
|
|
|
| 107 |
except json.JSONDecodeError:
|
| 108 |
logging.error(f"Error decoding JSON in local {DATA_FILE}. File might be corrupt. Attempting download.")
|
| 109 |
|
|
|
|
| 110 |
if download_db_from_hf(specific_file=DATA_FILE):
|
| 111 |
try:
|
|
|
|
| 112 |
with open(DATA_FILE, 'r', encoding='utf-8') as file:
|
| 113 |
data = json.load(file)
|
| 114 |
logging.info(f"Data loaded successfully from {DATA_FILE} after download.")
|
|
|
|
| 129 |
return default_data
|
| 130 |
else:
|
| 131 |
logging.error(f"Failed to download {DATA_FILE} from HF after retries. Using empty default data structure.")
|
| 132 |
+
return default_data
|
| 133 |
|
| 134 |
def save_data(data):
|
| 135 |
try:
|
|
|
|
| 136 |
if not isinstance(data, dict):
|
| 137 |
logging.error("Attempted to save invalid data structure (not a dict). Aborting save.")
|
| 138 |
return
|
|
|
|
| 158 |
except json.JSONDecodeError:
|
| 159 |
logging.error(f"Error decoding JSON in local {USERS_FILE}. File might be corrupt. Attempting download.")
|
| 160 |
|
|
|
|
| 161 |
if download_db_from_hf(specific_file=USERS_FILE):
|
| 162 |
try:
|
|
|
|
| 163 |
with open(USERS_FILE, 'r', encoding='utf-8') as file:
|
| 164 |
users = json.load(file)
|
| 165 |
logging.info(f"Users loaded successfully from {USERS_FILE} after download.")
|
|
|
|
| 175 |
return default_users
|
| 176 |
else:
|
| 177 |
logging.error(f"Failed to download {USERS_FILE} from HF after retries. Using empty default user structure.")
|
| 178 |
+
return default_users
|
| 179 |
|
| 180 |
def save_users(users):
|
| 181 |
try:
|
|
|
|
| 729 |
alert("Корзина пуста! Добавьте товары перед заказом.");
|
| 730 |
return;
|
| 731 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 732 |
|
| 733 |
+
try {
|
| 734 |
+
const cartJson = JSON.stringify(cart);
|
| 735 |
+
const encodedCart = btoa(unescape(encodeURIComponent(cartJson))); // Base64 encode
|
| 736 |
+
|
| 737 |
+
if (encodedCart.length > 1800) { // Basic check for URL length limits
|
| 738 |
+
alert("Ошибка: Корзина слишком большая для создания ссылки. Пожалуйста, свяжитесь с нами напрямую.");
|
| 739 |
+
return;
|
| 740 |
+
}
|
| 741 |
+
|
| 742 |
+
const orderViewUrl = `${window.location.origin}/order_view/${encodedCart}`;
|
| 743 |
|
| 744 |
+
let messageText = "🛍️ *Новый Заказ Soola Cosmetics* 🛍️%0A";
|
| 745 |
+
messageText += "----------------------------------------%0A";
|
| 746 |
+
messageText += "*Пожалуйста, посмотрите детали заказа по ссылке ниже:*%0A";
|
| 747 |
+
messageText += encodeURIComponent(orderViewUrl) + "%0A"; // URL encode the link itself for safety
|
| 748 |
+
messageText += "----------------------------------------%0A";
|
| 749 |
+
|
| 750 |
+
if (userInfo && userInfo.login) {
|
| 751 |
+
messageText += "*Клиент:*%0A";
|
| 752 |
+
messageText += `${userInfo.first_name || ''} ${userInfo.last_name || ''} (${userInfo.login})%0A`;
|
| 753 |
+
} else {
|
| 754 |
+
messageText += "*Клиент не авторизован*%0A";
|
| 755 |
+
}
|
| 756 |
+
messageText += "----------------------------------------%0A";
|
| 757 |
+
const now = new Date();
|
| 758 |
+
const dateTimeString = now.toLocaleString('ru-RU', { dateStyle: 'short', timeStyle: 'short' });
|
| 759 |
+
messageText += `Дата запроса: ${dateTimeString}`;
|
| 760 |
+
|
| 761 |
+
const whatsappNumber = "996997703090";
|
| 762 |
+
const whatsappUrl = `https://api.whatsapp.com/send?phone=${whatsappNumber}&text=${messageText}`;
|
| 763 |
+
|
| 764 |
+
window.open(whatsappUrl, '_blank');
|
| 765 |
+
|
| 766 |
+
} catch (e) {
|
| 767 |
+
console.error("Error creating WhatsApp order link:", e);
|
| 768 |
+
alert("Произошла ошибка при формировании ссылки для заказа. Пожалуйста, попробуйте еще раз или свяжитесь с нами.");
|
| 769 |
+
}
|
| 770 |
}
|
| 771 |
|
| 772 |
|
|
|
|
| 945 |
currency_code=CURRENCY_CODE
|
| 946 |
)
|
| 947 |
|
| 948 |
+
ORDER_VIEW_TEMPLATE = '''
|
| 949 |
+
<!DOCTYPE html>
|
| 950 |
+
<html lang="ru">
|
| 951 |
+
<head>
|
| 952 |
+
<meta charset="UTF-8">
|
| 953 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 954 |
+
<title>Детали Заказа - Soola Cosmetics</title>
|
| 955 |
+
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;600&display=swap" rel="stylesheet">
|
| 956 |
+
<style>
|
| 957 |
+
body { font-family: 'Poppins', sans-serif; background: #f0f9f4; color: #2d332f; line-height: 1.6; padding: 20px; }
|
| 958 |
+
.container { max-width: 800px; margin: 20px auto; background: #fff; padding: 30px; border-radius: 10px; box-shadow: 0 3px 15px rgba(0,0,0,0.1); }
|
| 959 |
+
h1 { color: #1C6758; text-align: center; margin-bottom: 25px; font-size: 1.8rem; font-weight: 600; }
|
| 960 |
+
h2 { color: #164B41; margin-top: 30px; margin-bottom: 15px; font-size: 1.3rem; border-bottom: 1px solid #d1e7dd; padding-bottom: 8px; }
|
| 961 |
+
.order-item { display: flex; gap: 15px; align-items: center; padding: 15px 0; border-bottom: 1px solid #e1f0e9; }
|
| 962 |
+
.order-item:last-child { border-bottom: none; }
|
| 963 |
+
.order-item img { width: 70px; height: 70px; object-fit: contain; border-radius: 8px; background-color: #fff; padding: 5px; border: 1px solid #e1f0e9; flex-shrink: 0;}
|
| 964 |
+
.item-details { flex-grow: 1; }
|
| 965 |
+
.item-details strong { display: block; font-size: 1.1rem; color: #2d332f; margin-bottom: 5px; }
|
| 966 |
+
.item-meta { font-size: 0.9rem; color: #5e6e68; }
|
| 967 |
+
.item-total { font-weight: bold; text-align: right; font-size: 1rem; color: #1C6758; }
|
| 968 |
+
.summary { margin-top: 30px; padding-top: 20px; border-top: 2px solid #1C6758; text-align: right; }
|
| 969 |
+
.summary p { margin: 8px 0; font-size: 1.1rem; }
|
| 970 |
+
.summary strong { font-size: 1.3rem; color: #1C6758; }
|
| 971 |
+
.customer-info { margin-top: 20px; background-color: #f8fcfb; padding: 15px; border-radius: 8px; border: 1px solid #d1e7dd; font-size: 0.95rem; }
|
| 972 |
+
.customer-info p { margin: 4px 0; }
|
| 973 |
+
.footer-note { text-align: center; margin-top: 30px; font-size: 0.85rem; color: #7a8d85; }
|
| 974 |
+
.error-message { color: #721c24; background-color: #f8d7da; border: 1px solid #f5c6cb; padding: 15px; border-radius: 8px; text-align: center; font-weight: 500; }
|
| 975 |
+
</style>
|
| 976 |
+
</head>
|
| 977 |
+
<body>
|
| 978 |
+
<div class="container">
|
| 979 |
+
<h1>Детали Заказа</h1>
|
| 980 |
+
|
| 981 |
+
{% if error %}
|
| 982 |
+
<p class="error-message">{{ error }}</p>
|
| 983 |
+
{% else %}
|
| 984 |
+
{% if cart_items %}
|
| 985 |
+
<h2>Товары в заказе</h2>
|
| 986 |
+
{% for item in cart_items %}
|
| 987 |
+
<div class="order-item">
|
| 988 |
+
<img src="{{ item.photo_url }}" alt="{{ item.name }}">
|
| 989 |
+
<div class="item-details">
|
| 990 |
+
<strong>{{ item.name }}{{ item.color_text }}</strong>
|
| 991 |
+
<p class="item-meta">Цена: {{ "%.2f"|format(item.price) }} {{ currency_code }}</p>
|
| 992 |
+
<p class="item-meta">Количество: {{ item.quantity }}</p>
|
| 993 |
+
</div>
|
| 994 |
+
<div class="item-total">{{ "%.2f"|format(item.item_total) }} {{ currency_code }}</div>
|
| 995 |
+
</div>
|
| 996 |
+
{% endfor %}
|
| 997 |
+
|
| 998 |
+
<div class="summary">
|
| 999 |
+
<p><strong>Общая сумма заказа: {{ "%.2f"|format(total_price) }} {{ currency_code }}</strong></p>
|
| 1000 |
+
</div>
|
| 1001 |
+
|
| 1002 |
+
{% if user_info %}
|
| 1003 |
+
<div class="customer-info">
|
| 1004 |
+
<h2>Информация о клиенте</h2>
|
| 1005 |
+
<p><strong>Имя:</strong> {{ user_info.get('first_name', '') }} {{ user_info.get('last_name', '') }}</p>
|
| 1006 |
+
<p><strong>Логин:</strong> {{ user_info.get('login', 'N/A') }}</p>
|
| 1007 |
+
<p><strong>Телефон:</strong> {{ user_info.get('phone', 'Не указан') }}</p>
|
| 1008 |
+
<p><strong>Страна:</strong> {{ user_info.get('country', 'Не указана') }}</p>
|
| 1009 |
+
<p><strong>Город:</strong> {{ user_info.get('city', 'Не указан') }}</p>
|
| 1010 |
+
</div>
|
| 1011 |
+
{% endif %}
|
| 1012 |
+
|
| 1013 |
+
{% else %}
|
| 1014 |
+
<p style="text-align: center; padding: 30px;">Не удалось загрузить детали заказа или корзина пуста.</p>
|
| 1015 |
+
{% endif %}
|
| 1016 |
+
<p class="footer-note">Заказ сформирован: {{ generation_time }}</p>
|
| 1017 |
+
{% endif %}
|
| 1018 |
+
</div>
|
| 1019 |
+
</body>
|
| 1020 |
+
</html>
|
| 1021 |
+
'''
|
| 1022 |
+
|
| 1023 |
+
@app.route('/order_view/<encoded_cart>')
|
| 1024 |
+
def order_view(encoded_cart):
|
| 1025 |
+
cart_items_processed = []
|
| 1026 |
+
total_price = 0
|
| 1027 |
+
error_message = None
|
| 1028 |
+
user_info_data = session.get('user_info') # Get user info from session
|
| 1029 |
+
|
| 1030 |
+
try:
|
| 1031 |
+
decoded_bytes = base64.urlsafe_b64decode(encoded_cart + '=' * (-len(encoded_cart) % 4))
|
| 1032 |
+
cart_json = decoded_bytes.decode('utf-8')
|
| 1033 |
+
cart_items = json.loads(cart_json)
|
| 1034 |
+
|
| 1035 |
+
if not isinstance(cart_items, list):
|
| 1036 |
+
raise ValueError("Decoded data is not a list")
|
| 1037 |
+
|
| 1038 |
+
for item in cart_items:
|
| 1039 |
+
if not isinstance(item, dict) or 'name' not in item or 'price' not in item or 'quantity' not in item:
|
| 1040 |
+
logging.warning(f"Skipping invalid item in decoded cart: {item}")
|
| 1041 |
+
continue
|
| 1042 |
+
|
| 1043 |
+
item_total = float(item['price']) * int(item['quantity'])
|
| 1044 |
+
total_price += item_total
|
| 1045 |
+
photo_url = (f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/photos/{item['photo']}"
|
| 1046 |
+
if item.get('photo') else "https://via.placeholder.com/70x70.png?text=N/A")
|
| 1047 |
+
color_text = f" (Цвет: {item['color']})" if item.get('color') and item['color'] != 'N/A' else ''
|
| 1048 |
+
|
| 1049 |
+
cart_items_processed.append({
|
| 1050 |
+
'name': item['name'],
|
| 1051 |
+
'price': float(item['price']),
|
| 1052 |
+
'quantity': int(item['quantity']),
|
| 1053 |
+
'photo_url': photo_url,
|
| 1054 |
+
'color_text': color_text,
|
| 1055 |
+
'item_total': item_total
|
| 1056 |
+
})
|
| 1057 |
+
|
| 1058 |
+
except (base64.binascii.Error, UnicodeDecodeError, json.JSONDecodeError, ValueError, TypeError) as e:
|
| 1059 |
+
logging.error(f"Error decoding or processing order data from URL: {e}", exc_info=True)
|
| 1060 |
+
error_message = "Не удалось расшифровать или обработать данные заказа. Ссылка может быть повреждена или неверна."
|
| 1061 |
+
except Exception as e:
|
| 1062 |
+
logging.error(f"Unexpected error processing order view: {e}", exc_info=True)
|
| 1063 |
+
error_message = "Произошла непредвиденная ошибка при отображении заказа."
|
| 1064 |
+
|
| 1065 |
+
generation_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
| 1066 |
+
|
| 1067 |
+
return render_template_string(
|
| 1068 |
+
ORDER_VIEW_TEMPLATE,
|
| 1069 |
+
cart_items=cart_items_processed,
|
| 1070 |
+
total_price=total_price,
|
| 1071 |
+
currency_code=CURRENCY_CODE,
|
| 1072 |
+
repo_id=REPO_ID,
|
| 1073 |
+
error=error_message,
|
| 1074 |
+
user_info=user_info_data,
|
| 1075 |
+
generation_time=generation_time
|
| 1076 |
+
)
|
| 1077 |
+
|
| 1078 |
+
|
| 1079 |
LOGIN_TEMPLATE = '''
|
| 1080 |
<!DOCTYPE html>
|
| 1081 |
<html lang="ru">
|
|
|
|
| 1589 |
if (group) {
|
| 1590 |
const container = group.parentNode;
|
| 1591 |
group.remove();
|
| 1592 |
+
// Add a new empty input if the last one was removed, except for the add form
|
| 1593 |
+
if (container && container.children.length === 0 && !container.id.startsWith('add-color-inputs')) {
|
| 1594 |
const placeholderGroup = document.createElement('div');
|
| 1595 |
placeholderGroup.className = 'color-input-group';
|
| 1596 |
placeholderGroup.innerHTML = `
|
|
|
|
| 1610 |
|
| 1611 |
@app.route('/admin', methods=['GET', 'POST'])
|
| 1612 |
def admin():
|
|
|
|
| 1613 |
data = load_data()
|
| 1614 |
users = load_users()
|
|
|
|
| 1615 |
products = data.get('products', [])
|
| 1616 |
categories = data.get('categories', [])
|
| 1617 |
|
|
|
|
| 1625 |
if category_name and category_name not in categories:
|
| 1626 |
categories.append(category_name)
|
| 1627 |
categories.sort()
|
| 1628 |
+
data['categories'] = categories
|
| 1629 |
save_data(data)
|
| 1630 |
logging.info(f"Category '{category_name}' added.")
|
| 1631 |
flash(f"Категория '{category_name}' ус��ешно добавлена.", 'success')
|
|
|
|
| 1645 |
if product.get('category') == category_to_delete:
|
| 1646 |
product['category'] = 'Без категории'
|
| 1647 |
updated_count += 1
|
| 1648 |
+
data['categories'] = categories
|
| 1649 |
+
data['products'] = products
|
| 1650 |
save_data(data)
|
| 1651 |
logging.info(f"Category '{category_to_delete}' deleted. Updated products: {updated_count}.")
|
| 1652 |
flash(f"Категория '{category_to_delete}' удалена.", 'success')
|
|
|
|
| 1690 |
if photo and photo.filename:
|
| 1691 |
try:
|
| 1692 |
ext = os.path.splitext(photo.filename)[1].lower()
|
|
|
|
| 1693 |
if ext not in ['.jpg', '.jpeg', '.png', '.gif', '.webp']:
|
| 1694 |
logging.warning(f"Skipping non-image file upload: {photo.filename}")
|
| 1695 |
flash(f"Файл {photo.filename} не является изображением и был пропущен.", "warning")
|
|
|
|
| 1714 |
except Exception as e:
|
| 1715 |
logging.error(f"Error uploading photo {photo.filename} to HF: {e}", exc_info=True)
|
| 1716 |
flash(f"Ошибка при загрузке фото {photo.filename}.", 'error')
|
|
|
|
| 1717 |
if os.path.exists(temp_path):
|
| 1718 |
try: os.remove(temp_path)
|
| 1719 |
except OSError: pass
|
| 1720 |
elif photo and not photo.filename:
|
| 1721 |
logging.warning("Received an empty photo file object when adding product.")
|
| 1722 |
try:
|
|
|
|
| 1723 |
if os.path.exists(uploads_dir) and not os.listdir(uploads_dir):
|
| 1724 |
os.rmdir(uploads_dir)
|
| 1725 |
except OSError as e:
|
|
|
|
| 1732 |
'in_stock': in_stock, 'is_top': is_top
|
| 1733 |
}
|
| 1734 |
products.append(new_product)
|
| 1735 |
+
data['products'] = products
|
| 1736 |
save_data(data)
|
| 1737 |
logging.info(f"Product '{name}' added.")
|
| 1738 |
flash(f"Товар '{name}' успешно добавлен.", 'success')
|
|
|
|
| 1745 |
|
| 1746 |
try:
|
| 1747 |
index = int(index_str)
|
|
|
|
| 1748 |
if not (0 <= index < len(products)):
|
| 1749 |
raise IndexError("Product index out of range")
|
| 1750 |
product_to_edit = products[index]
|
|
|
|
| 1839 |
elif uploaded_count == 0 and any(f.filename for f in photos_files):
|
| 1840 |
flash("Не удалось загрузить новые фотографии.", "error")
|
| 1841 |
|
|
|
|
| 1842 |
products[index] = product_to_edit
|
| 1843 |
+
data['products'] = products
|
| 1844 |
save_data(data)
|
| 1845 |
logging.info(f"Product '{original_name}' (index {index}) updated to '{product_to_edit['name']}'.")
|
| 1846 |
flash(f"Товар '{product_to_edit['name']}' успешно обновлен.", 'success')
|
|
|
|
| 1852 |
return redirect(url_for('admin'))
|
| 1853 |
try:
|
| 1854 |
index = int(index_str)
|
|
|
|
| 1855 |
if not (0 <= index < len(products)): raise IndexError("Product index out of range")
|
| 1856 |
deleted_product = products.pop(index)
|
| 1857 |
product_name = deleted_product.get('name', 'N/A')
|
|
|
|
| 1873 |
logging.error(f"Error deleting photos {photos_to_delete} for product '{product_name}' from HF: {e}", exc_info=True)
|
| 1874 |
flash(f"Не удалось удалить фото для товара '{product_name}' с сервера.", "warning")
|
| 1875 |
|
| 1876 |
+
data['products'] = products
|
| 1877 |
save_data(data)
|
| 1878 |
logging.info(f"Product '{product_name}' (original index {index}) deleted.")
|
| 1879 |
flash(f"Товар '{product_name}' удален.", 'success')
|
|
|
|
| 1929 |
flash(f"Произошла внутренняя ошибка при выполнении действия '{action}'. Подробности в логе сервера.", 'error')
|
| 1930 |
return redirect(url_for('admin'))
|
| 1931 |
|
|
|
|
|
|
|
|
|
|
| 1932 |
current_data = load_data()
|
| 1933 |
current_users = load_users()
|
| 1934 |
display_products = current_data.get('products', [])
|
|
|
|
| 1969 |
return redirect(url_for('admin'))
|
| 1970 |
|
| 1971 |
if __name__ == '__main__':
|
|
|
|
| 1972 |
load_data()
|
| 1973 |
load_users()
|
| 1974 |
|
|
|
|
| 1981 |
|
| 1982 |
port = int(os.environ.get('PORT', 7860))
|
| 1983 |
logging.info(f"Starting Flask app on host 0.0.0.0 and port {port}")
|
|
|
|
|
|
|
| 1984 |
app.run(debug=False, host='0.0.0.0', port=port)
|
| 1985 |
|
| 1986 |
+
# --- END OF FILE app.py ---
|