Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -5,7 +5,7 @@ import os
|
|
| 5 |
import logging
|
| 6 |
import threading
|
| 7 |
import time
|
| 8 |
-
import
|
| 9 |
from datetime import datetime
|
| 10 |
from huggingface_hub import HfApi, hf_hub_download
|
| 11 |
from huggingface_hub.utils import RepositoryNotFoundError, HfHubHTTPError
|
|
@@ -13,14 +13,9 @@ from werkzeug.utils import secure_filename
|
|
| 13 |
from dotenv import load_dotenv
|
| 14 |
import requests
|
| 15 |
import uuid
|
| 16 |
-
import
|
| 17 |
-
from aiohttp import TCPConnector
|
| 18 |
-
from aiogram import Bot, Dispatcher, types
|
| 19 |
-
from aiogram.client.session.aiohttp import AiohttpSession
|
| 20 |
from aiogram.filters import CommandStart
|
| 21 |
-
from aiogram.types import
|
| 22 |
-
from aiogram.client.bot import DefaultBotProperties
|
| 23 |
-
from aiogram.enums import ParseMode
|
| 24 |
|
| 25 |
load_dotenv()
|
| 26 |
|
|
@@ -33,9 +28,7 @@ SYNC_FILES = [DATA_FILE]
|
|
| 33 |
REPO_ID = "Kgshop/aiocult"
|
| 34 |
HF_TOKEN_WRITE = os.getenv("HF_TOKEN")
|
| 35 |
HF_TOKEN_READ = os.getenv("HF_TOKEN_READ")
|
| 36 |
-
|
| 37 |
-
BOT_TOKEN = os.getenv("BOT_TOKEN", "8283649768:AAGYWatM-nUVQirgBiBwoAtWZgzfp3QnQjY")
|
| 38 |
-
WEB_APP_URL = "https://shveiauto-wall.hf.space/"
|
| 39 |
|
| 40 |
STORE_ADDRESS = " Алматы, «Байсат» 1 этаж, синий сектор 117-118 Бутик "
|
| 41 |
|
|
@@ -54,23 +47,54 @@ STATUS_MAP_RU = {
|
|
| 54 |
|
| 55 |
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
| 56 |
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
bot =
|
| 60 |
-
dp =
|
| 61 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 62 |
|
| 63 |
def download_db_from_hf(specific_file=None, retries=DOWNLOAD_RETRIES, delay=DOWNLOAD_DELAY):
|
|
|
|
|
|
|
|
|
|
| 64 |
token_to_use = HF_TOKEN_READ if HF_TOKEN_READ else HF_TOKEN_WRITE
|
| 65 |
|
| 66 |
files_to_download = [specific_file] if specific_file else SYNC_FILES
|
|
|
|
| 67 |
all_successful = True
|
| 68 |
|
| 69 |
for file_name in files_to_download:
|
| 70 |
success = False
|
| 71 |
for attempt in range(retries + 1):
|
| 72 |
try:
|
| 73 |
-
|
|
|
|
| 74 |
repo_id=REPO_ID,
|
| 75 |
filename=file_name,
|
| 76 |
repo_type="dataset",
|
|
@@ -80,38 +104,50 @@ def download_db_from_hf(specific_file=None, retries=DOWNLOAD_RETRIES, delay=DOWN
|
|
| 80 |
force_download=True,
|
| 81 |
resume_download=False
|
| 82 |
)
|
|
|
|
| 83 |
success = True
|
| 84 |
break
|
| 85 |
except RepositoryNotFoundError:
|
|
|
|
| 86 |
return False
|
| 87 |
except HfHubHTTPError as e:
|
| 88 |
if e.response.status_code == 404:
|
|
|
|
| 89 |
if attempt == 0 and not os.path.exists(file_name):
|
| 90 |
try:
|
| 91 |
if file_name == DATA_FILE:
|
| 92 |
with open(file_name, 'w', encoding='utf-8') as f:
|
| 93 |
-
json.dump({'products': [], 'categories': [], 'orders': {}, '
|
| 94 |
-
|
| 95 |
-
|
|
|
|
| 96 |
success = False
|
| 97 |
break
|
| 98 |
-
|
| 99 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 100 |
|
| 101 |
if attempt < retries:
|
| 102 |
time.sleep(delay)
|
| 103 |
|
| 104 |
if not success:
|
|
|
|
| 105 |
all_successful = False
|
| 106 |
|
|
|
|
| 107 |
return all_successful
|
| 108 |
|
| 109 |
def upload_db_to_hf(specific_file=None):
|
| 110 |
if not HF_TOKEN_WRITE:
|
|
|
|
| 111 |
return
|
| 112 |
try:
|
| 113 |
api = HfApi()
|
| 114 |
files_to_upload = [specific_file] if specific_file else SYNC_FILES
|
|
|
|
| 115 |
|
| 116 |
for file_name in files_to_upload:
|
| 117 |
if os.path.exists(file_name):
|
|
@@ -124,19 +160,26 @@ def upload_db_to_hf(specific_file=None):
|
|
| 124 |
token=HF_TOKEN_WRITE,
|
| 125 |
commit_message=f"Sync {file_name} {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
|
| 126 |
)
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 131 |
|
| 132 |
def periodic_backup():
|
| 133 |
backup_interval = 1800
|
|
|
|
| 134 |
while True:
|
| 135 |
time.sleep(backup_interval)
|
|
|
|
| 136 |
upload_db_to_hf()
|
|
|
|
| 137 |
|
| 138 |
-
def
|
| 139 |
-
default_data = {'products': [], 'categories': [], 'orders': {}, '
|
| 140 |
try:
|
| 141 |
with open(DATA_FILE, 'r', encoding='utf-8') as file:
|
| 142 |
data = json.load(file)
|
|
@@ -145,102 +188,89 @@ def load_data():
|
|
| 145 |
if 'products' not in data: data['products'] = []
|
| 146 |
if 'categories' not in data: data['categories'] = []
|
| 147 |
if 'orders' not in data: data['orders'] = {}
|
| 148 |
-
if '
|
| 149 |
return data
|
| 150 |
except (FileNotFoundError, json.JSONDecodeError):
|
| 151 |
-
|
| 152 |
|
| 153 |
-
|
|
|
|
|
|
|
| 154 |
try:
|
| 155 |
with open(DATA_FILE, 'r', encoding='utf-8') as file:
|
| 156 |
data = json.load(file)
|
|
|
|
| 157 |
if not isinstance(data, dict):
|
| 158 |
-
|
|
|
|
| 159 |
if 'products' not in data: data['products'] = []
|
| 160 |
if 'categories' not in data: data['categories'] = []
|
| 161 |
if 'orders' not in data: data['orders'] = {}
|
| 162 |
-
if '
|
| 163 |
return data
|
| 164 |
-
except
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
|
|
|
|
|
|
| 168 |
try:
|
| 169 |
-
with open(DATA_FILE, '
|
| 170 |
-
json.
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 174 |
|
| 175 |
-
def
|
| 176 |
try:
|
| 177 |
-
if not isinstance(data, dict):
|
| 178 |
-
return
|
| 179 |
if 'products' not in data: data['products'] = []
|
| 180 |
if 'categories' not in data: data['categories'] = []
|
| 181 |
if 'orders' not in data: data['orders'] = {}
|
| 182 |
-
if '
|
| 183 |
|
| 184 |
with open(DATA_FILE, 'w', encoding='utf-8') as file:
|
| 185 |
json.dump(data, file, ensure_ascii=False, indent=4)
|
| 186 |
upload_db_to_hf(specific_file=DATA_FILE)
|
| 187 |
-
except Exception:
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
async def send_new_product_notification(product_name, price, photo_filename=None):
|
| 191 |
-
data = load_data()
|
| 192 |
-
users = data.get('bot_users', [])
|
| 193 |
-
if not users:
|
| 194 |
-
return
|
| 195 |
-
|
| 196 |
-
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
| 197 |
-
[InlineKeyboardButton(text="Посмотреть новинку", web_app=WebAppInfo(url=WEB_APP_URL))]
|
| 198 |
-
])
|
| 199 |
-
message_text = f"✨ <b>Поступление нового товара!</b> ✨\n\nВ нашем каталоге появился <b>{product_name}</b> по цене {price} {CURRENCY_CODE}."
|
| 200 |
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
photo_url = f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/photos/{photo_filename}"
|
| 205 |
-
await bot.send_photo(chat_id=user_id, photo=photo_url, caption=message_text, reply_markup=keyboard)
|
| 206 |
-
else:
|
| 207 |
-
await bot.send_message(user_id, message_text, reply_markup=keyboard)
|
| 208 |
-
await asyncio.sleep(0.1)
|
| 209 |
-
except Exception:
|
| 210 |
-
pass
|
| 211 |
-
|
| 212 |
-
@dp.message(CommandStart())
|
| 213 |
-
async def send_welcome(message: types.Message):
|
| 214 |
-
user_id = message.from_user.id
|
| 215 |
-
data = load_data()
|
| 216 |
-
if user_id not in data.get('bot_users', []):
|
| 217 |
-
data.setdefault('bot_users', []).append(user_id)
|
| 218 |
-
save_data(data)
|
| 219 |
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
|
| 225 |
-
async def
|
| 226 |
-
|
|
|
|
| 227 |
try:
|
| 228 |
-
await bot.
|
| 229 |
-
|
| 230 |
-
|
| 231 |
except Exception as e:
|
| 232 |
-
logging.
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
try:
|
| 236 |
-
await dp.start_polling(bot)
|
| 237 |
-
except Exception as e:
|
| 238 |
-
logging.error(f"Polling failed to start: {e}")
|
| 239 |
-
|
| 240 |
-
def run_bot():
|
| 241 |
-
time.sleep(5)
|
| 242 |
-
asyncio.set_event_loop(bot_loop)
|
| 243 |
-
bot_loop.run_until_complete(main_bot())
|
| 244 |
|
| 245 |
CATALOG_TEMPLATE = '''
|
| 246 |
<!DOCTYPE html>
|
|
@@ -252,10 +282,9 @@ CATALOG_TEMPLATE = '''
|
|
| 252 |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
|
| 253 |
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap" rel="stylesheet">
|
| 254 |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/Swiper/10.2.0/swiper-bundle.min.css">
|
| 255 |
-
<script src="https://telegram.org/js/telegram-web-app.js"></script>
|
| 256 |
<style>
|
| 257 |
:root {
|
| 258 |
-
--primary-color: #007AFF;
|
| 259 |
--primary-dark: #0056b3;
|
| 260 |
--surface-color: #FFFFFF;
|
| 261 |
--surface-dark: #21252B;
|
|
@@ -280,10 +309,6 @@ CATALOG_TEMPLATE = '''
|
|
| 280 |
.theme-toggle { background: none; border: none; font-size: 1.3rem; cursor: pointer; color: var(--primary-color); padding: 5px; }
|
| 281 |
.theme-toggle:hover { opacity: 0.8; }
|
| 282 |
|
| 283 |
-
#user-info { display: none; align-items: center; gap: 8px; font-size: 0.9rem; }
|
| 284 |
-
#user-info img { width: 35px; height: 35px; border-radius: 50%; }
|
| 285 |
-
#user-info span { font-weight: 500; }
|
| 286 |
-
|
| 287 |
.store-address { padding: 12px 15px; text-align: center; background-color: var(--surface-color); border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.05); font-size: 0.9rem; color: #555; margin: 15px; }
|
| 288 |
body.dark-mode .store-address { background-color: var(--surface-dark); color: var(--text-color-dark); box-shadow: 0 1px 3px rgba(255,255,255,0.1); }
|
| 289 |
|
|
@@ -311,7 +336,7 @@ CATALOG_TEMPLATE = '''
|
|
| 311 |
.product h2 { font-size: 0.95rem; font-weight: 500; margin: 0 0 5px 0; text-align: center; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: var(--text-color); }
|
| 312 |
body.dark-mode .product h2 { color: var(--text-color-dark); }
|
| 313 |
.product-price { font-size: 1.1rem; color: var(--primary-color); font-weight: 700; text-align: center; margin: 5px 0; }
|
| 314 |
-
.product-description { display: none; }
|
| 315 |
.product-actions { padding: 0 10px 10px 10px; display: flex; flex-direction: column; gap: 6px; }
|
| 316 |
|
| 317 |
.product-button { display: block; width: 100%; padding: 8px; border: none; border-radius: 8px; background-color: var(--primary-color); color: white; font-size: 0.85rem; font-weight: 500; cursor: pointer; transition: all 0.2s ease; text-align: center; text-decoration: none; }
|
|
@@ -369,10 +394,6 @@ CATALOG_TEMPLATE = '''
|
|
| 369 |
<div class="logo-title-container" style="display: flex; align-items: center; gap: 10px;">
|
| 370 |
<img src="https://huggingface.co/spaces/Kgshop/mobilnymir/resolve/main/IMG_20251019_120056_121.jpg" alt="Мобильный мир Logo">
|
| 371 |
<h1>Мобильный мир</h1>
|
| 372 |
-
</div>
|
| 373 |
-
<div id="user-info">
|
| 374 |
-
<img id="user-avatar" src="" alt="avatar">
|
| 375 |
-
<span id="user-name"></span>
|
| 376 |
</div>
|
| 377 |
<button class="theme-toggle" onclick="toggleTheme()" aria-label="Переключить тему">
|
| 378 |
<i class="fas fa-moon"></i>
|
|
@@ -630,7 +651,7 @@ CATALOG_TEMPLATE = '''
|
|
| 630 |
return;
|
| 631 |
}
|
| 632 |
|
| 633 |
-
const cartItemId = `${product.name}-${color}-${model}`;
|
| 634 |
const existingItemIndex = cart.findIndex(item => item.id === cartItemId);
|
| 635 |
|
| 636 |
if (existingItemIndex > -1) {
|
|
@@ -643,7 +664,7 @@ CATALOG_TEMPLATE = '''
|
|
| 643 |
photo: product.photos && product.photos.length > 0 ? product.photos[0] : null,
|
| 644 |
quantity: quantity,
|
| 645 |
color: color,
|
| 646 |
-
model: model
|
| 647 |
});
|
| 648 |
}
|
| 649 |
|
|
@@ -690,7 +711,7 @@ CATALOG_TEMPLATE = '''
|
|
| 690 |
|
| 691 |
let variantText = [];
|
| 692 |
if (item.color !== 'N/A') variantText.push(`Цвет: ${item.color}`);
|
| 693 |
-
if (item.model !== 'N/A') variantText.push(`Модель: ${item.model}`);
|
| 694 |
|
| 695 |
const variantDisplay = variantText.length > 0 ? ` (${variantText.join(', ')})` : '';
|
| 696 |
|
|
@@ -787,7 +808,7 @@ CATALOG_TEMPLATE = '''
|
|
| 787 |
if (existingNoResults) existingNoResults.remove();
|
| 788 |
|
| 789 |
const allProductElements = document.querySelectorAll('.products-grid .product');
|
| 790 |
-
if (allProductElements.length === 0 && products.length > 0) return;
|
| 791 |
|
| 792 |
document.querySelectorAll('.products-grid .product').forEach(productElement => {
|
| 793 |
const name = productElement.getAttribute('data-name');
|
|
@@ -865,40 +886,11 @@ CATALOG_TEMPLATE = '''
|
|
| 865 |
notification.addEventListener('transitionend', () => notification.remove());
|
| 866 |
}, duration);
|
| 867 |
}
|
| 868 |
-
|
| 869 |
-
function initTelegramWebApp() {
|
| 870 |
-
if (window.Telegram && window.Telegram.WebApp) {
|
| 871 |
-
const tg = window.Telegram.WebApp;
|
| 872 |
-
tg.ready();
|
| 873 |
-
|
| 874 |
-
if (tg.initDataUnsafe && tg.initDataUnsafe.user) {
|
| 875 |
-
const user = tg.initDataUnsafe.user;
|
| 876 |
-
const userInfoDiv = document.getElementById('user-info');
|
| 877 |
-
const userAvatarImg = document.getElementById('user-avatar');
|
| 878 |
-
const userNameSpan = document.getElementById('user-name');
|
| 879 |
-
|
| 880 |
-
if (userInfoDiv && userAvatarImg && userNameSpan) {
|
| 881 |
-
if (user.photo_url) {
|
| 882 |
-
userAvatarImg.src = user.photo_url;
|
| 883 |
-
} else {
|
| 884 |
-
userAvatarImg.style.display = 'none';
|
| 885 |
-
}
|
| 886 |
-
userNameSpan.textContent = user.first_name + (user.last_name ? ' ' + user.last_name : '');
|
| 887 |
-
userInfoDiv.style.display = 'flex';
|
| 888 |
-
}
|
| 889 |
-
} else {
|
| 890 |
-
console.log("Telegram Web App user data not found.");
|
| 891 |
-
}
|
| 892 |
-
} else {
|
| 893 |
-
console.log("Telegram Web App script not loaded or not in Telegram environment.");
|
| 894 |
-
}
|
| 895 |
-
}
|
| 896 |
|
| 897 |
document.addEventListener('DOMContentLoaded', () => {
|
| 898 |
applyInitialTheme();
|
| 899 |
updateCartButton();
|
| 900 |
setupFilters();
|
| 901 |
-
initTelegramWebApp();
|
| 902 |
|
| 903 |
window.addEventListener('click', function(event) {
|
| 904 |
if (event.target.classList.contains('modal')) {
|
|
@@ -1102,7 +1094,7 @@ ADMIN_TEMPLATE = '''
|
|
| 1102 |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
|
| 1103 |
<style>
|
| 1104 |
:root {
|
| 1105 |
-
--primary-color: #007AFF;
|
| 1106 |
--primary-dark: #0056b3;
|
| 1107 |
--surface-color: #FFFFFF;
|
| 1108 |
--surface-dark: #21252B;
|
|
@@ -1176,10 +1168,10 @@ ADMIN_TEMPLATE = '''
|
|
| 1176 |
.status-indicator.out-of-stock { background-color: #fed7d7; color: #c53030; }
|
| 1177 |
.status-indicator.top-product { background-color: #fff3c4; color: #9c4221; margin-left: 5px;}
|
| 1178 |
|
| 1179 |
-
.status-indicator.new { background-color: #ffe0b2; color: #e65100; }
|
| 1180 |
-
.status-indicator.accepted { background-color: #b3e5fc; color: #0277bd; }
|
| 1181 |
-
.status-indicator.prepared { background-color: #c6f6d5; color: #2f855a; }
|
| 1182 |
-
.status-indicator.shipped { background-color: #a7f3d0; color: #065f46; }
|
| 1183 |
|
| 1184 |
details { background-color: var(--surface-color); border: 1px solid var(--border-color); border-radius: 8px; margin-bottom: 10px; }
|
| 1185 |
body.dark-mode details { background-color: var(--surface-dark); border-color: var(--border-color-dark); }
|
|
@@ -1231,6 +1223,24 @@ ADMIN_TEMPLATE = '''
|
|
| 1231 |
{% endfor %}
|
| 1232 |
{% endif %}
|
| 1233 |
{% endwith %}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1234 |
|
| 1235 |
<div class="section">
|
| 1236 |
<h2><i class="fas fa-history"></i> История заказов</h2>
|
|
@@ -1562,7 +1572,7 @@ ADMIN_TEMPLATE = '''
|
|
| 1562 |
if (container) {
|
| 1563 |
const existingInputs = container.querySelectorAll('input[name="' + fieldName + '"]');
|
| 1564 |
if (existingInputs.length === 1 && existingInputs[0].value.trim() === '') {
|
| 1565 |
-
existingInputs[0].value = '';
|
| 1566 |
}
|
| 1567 |
|
| 1568 |
const newInputGroup = document.createElement('div');
|
|
@@ -1632,6 +1642,7 @@ def product_detail(index):
|
|
| 1632 |
try:
|
| 1633 |
product = products_sorted[index]
|
| 1634 |
except IndexError:
|
|
|
|
| 1635 |
return "Товар не найден или отсутствует в наличии.", 404
|
| 1636 |
|
| 1637 |
products_index = index
|
|
@@ -1649,6 +1660,7 @@ def create_order():
|
|
| 1649 |
order_data = request.get_json()
|
| 1650 |
|
| 1651 |
if not order_data or 'cart' not in order_data or not order_data['cart']:
|
|
|
|
| 1652 |
return jsonify({"error": "Корзина пуста или не передана."}), 400
|
| 1653 |
|
| 1654 |
cart_items = order_data['cart']
|
|
@@ -1657,6 +1669,7 @@ def create_order():
|
|
| 1657 |
processed_cart = []
|
| 1658 |
for item in cart_items:
|
| 1659 |
if not all(k in item for k in ('name', 'price', 'quantity')):
|
|
|
|
| 1660 |
return jsonify({"error": "Неверный формат товара в корзине."}), 400
|
| 1661 |
try:
|
| 1662 |
price = float(item['price'])
|
|
@@ -1678,6 +1691,7 @@ def create_order():
|
|
| 1678 |
"photo_url": photo_url
|
| 1679 |
})
|
| 1680 |
except (ValueError, TypeError) as e:
|
|
|
|
| 1681 |
return jsonify({"error": "Неверная цена или количество в товаре."}), 400
|
| 1682 |
|
| 1683 |
order_id = f"{datetime.now().strftime('%Y%m%d%H%M%S')}-{uuid.uuid4().hex[:6]}"
|
|
@@ -1699,9 +1713,11 @@ def create_order():
|
|
| 1699 |
|
| 1700 |
data['orders'][order_id] = new_order
|
| 1701 |
save_data(data)
|
|
|
|
| 1702 |
return jsonify({"order_id": order_id}), 201
|
| 1703 |
|
| 1704 |
except Exception as e:
|
|
|
|
| 1705 |
return jsonify({"error": "Ошибка сервера при сохранении заказа."}), 500
|
| 1706 |
|
| 1707 |
@app.route('/order/<order_id>')
|
|
@@ -1709,6 +1725,11 @@ def view_order(order_id):
|
|
| 1709 |
data = load_data()
|
| 1710 |
order = data.get('orders', {}).get(order_id)
|
| 1711 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1712 |
return render_template_string(ORDER_TEMPLATE,
|
| 1713 |
order=order,
|
| 1714 |
status_map_ru=STATUS_MAP_RU,
|
|
@@ -1721,6 +1742,7 @@ def admin():
|
|
| 1721 |
products = data.get('products', [])
|
| 1722 |
categories = data.get('categories', [])
|
| 1723 |
orders = data.get('orders', {})
|
|
|
|
| 1724 |
|
| 1725 |
needs_save = False
|
| 1726 |
for product in products:
|
|
@@ -1728,18 +1750,36 @@ def admin():
|
|
| 1728 |
product['id'] = str(uuid.uuid4())
|
| 1729 |
needs_save = True
|
| 1730 |
if needs_save:
|
|
|
|
| 1731 |
data['products'] = products
|
| 1732 |
save_data(data)
|
| 1733 |
|
| 1734 |
-
|
| 1735 |
if 'orders' not in data or not isinstance(data.get('orders'), dict):
|
| 1736 |
data['orders'] = {}
|
| 1737 |
|
| 1738 |
if request.method == 'POST':
|
| 1739 |
action = request.form.get('action')
|
|
|
|
| 1740 |
|
| 1741 |
try:
|
| 1742 |
-
if action == '
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1743 |
order_id = request.form.get('order_id')
|
| 1744 |
new_status = request.form.get('new_status')
|
| 1745 |
|
|
@@ -1748,9 +1788,12 @@ def admin():
|
|
| 1748 |
valid_statuses = list(STATUS_MAP_RU.keys())
|
| 1749 |
|
| 1750 |
if new_status in valid_statuses:
|
|
|
|
|
|
|
| 1751 |
order['status'] = new_status
|
| 1752 |
data['orders'][order_id] = order
|
| 1753 |
save_data(data)
|
|
|
|
| 1754 |
flash(f"Статус заказа №{order_id} успешно изменен на '{STATUS_MAP_RU.get(new_status)}'.", 'success')
|
| 1755 |
else:
|
| 1756 |
flash(f"Неверный статус: {new_status}", 'error')
|
|
@@ -1765,10 +1808,13 @@ def admin():
|
|
| 1765 |
categories.append(category_name)
|
| 1766 |
data['categories'] = categories
|
| 1767 |
save_data(data)
|
|
|
|
| 1768 |
flash(f"Категория '{category_name}' успешно добавлена.", 'success')
|
| 1769 |
elif not category_name:
|
|
|
|
| 1770 |
flash("Название категории не может быть пустым.", 'error')
|
| 1771 |
else:
|
|
|
|
| 1772 |
flash(f"Категория '{category_name}' уже существует.", 'error')
|
| 1773 |
|
| 1774 |
elif action == 'delete_category':
|
|
@@ -1783,8 +1829,10 @@ def admin():
|
|
| 1783 |
data['categories'] = categories
|
| 1784 |
data['products'] = products
|
| 1785 |
save_data(data)
|
|
|
|
| 1786 |
flash(f"Категория '{category_to_delete}' удалена. {updated_count} товаров обновлено.", 'success')
|
| 1787 |
else:
|
|
|
|
| 1788 |
flash(f"Не удалось удалить категорию '{category_to_delete}'.", 'error')
|
| 1789 |
|
| 1790 |
elif action == 'add_product':
|
|
@@ -1794,7 +1842,7 @@ def admin():
|
|
| 1794 |
category = request.form.get('category')
|
| 1795 |
photos_files = request.files.getlist('photos')
|
| 1796 |
colors = [c.strip() for c in request.form.getlist('colors') if c.strip()]
|
| 1797 |
-
models = [m.strip() for m in request.form.getlist('models') if m.strip()]
|
| 1798 |
in_stock = 'in_stock' in request.form
|
| 1799 |
is_top = 'is_top' in request.form
|
| 1800 |
|
|
@@ -1818,12 +1866,14 @@ def admin():
|
|
| 1818 |
uploaded_count = 0
|
| 1819 |
for photo in photos_files:
|
| 1820 |
if uploaded_count >= photo_limit:
|
|
|
|
| 1821 |
flash(f"Загружено только первые {photo_limit} фото.", "warning")
|
| 1822 |
break
|
| 1823 |
if photo and photo.filename:
|
| 1824 |
try:
|
| 1825 |
ext = os.path.splitext(photo.filename)[1].lower()
|
| 1826 |
if ext not in ['.jpg', '.jpeg', '.png', '.gif', '.webp']:
|
|
|
|
| 1827 |
flash(f"Файл {photo.filename} не является изображением и был пропущен.", "warning")
|
| 1828 |
continue
|
| 1829 |
|
|
@@ -1831,6 +1881,7 @@ def admin():
|
|
| 1831 |
photo_filename = f"{safe_name}_{datetime.now().strftime('%Y%m%d%H%M%S%f')}{ext}"
|
| 1832 |
temp_path = os.path.join(uploads_dir, photo_filename)
|
| 1833 |
photo.save(temp_path)
|
|
|
|
| 1834 |
api.upload_file(
|
| 1835 |
path_or_fileobj=temp_path,
|
| 1836 |
path_in_repo=f"photos/{photo_filename}",
|
|
@@ -1840,18 +1891,22 @@ def admin():
|
|
| 1840 |
commit_message=f"Add photo for product {name}"
|
| 1841 |
)
|
| 1842 |
photos_list.append(photo_filename)
|
|
|
|
| 1843 |
os.remove(temp_path)
|
| 1844 |
uploaded_count += 1
|
| 1845 |
-
except Exception:
|
|
|
|
| 1846 |
flash(f"Ошибка при загрузке фото {photo.filename}.", 'error')
|
| 1847 |
if os.path.exists(temp_path):
|
| 1848 |
try: os.remove(temp_path)
|
| 1849 |
except OSError: pass
|
|
|
|
|
|
|
| 1850 |
try:
|
| 1851 |
if os.path.exists(uploads_dir) and not os.listdir(uploads_dir):
|
| 1852 |
os.rmdir(uploads_dir)
|
| 1853 |
-
except OSError:
|
| 1854 |
-
|
| 1855 |
elif not HF_TOKEN_WRITE and photos_files and any(f.filename for f in photos_files):
|
| 1856 |
flash("HF_TOKEN (write) не настроен. Фотографии не были загружены.", "warning")
|
| 1857 |
|
|
@@ -1860,19 +1915,14 @@ def admin():
|
|
| 1860 |
'id': str(uuid.uuid4()),
|
| 1861 |
'name': name, 'price': price, 'description': description,
|
| 1862 |
'category': category if category in categories else 'Без категории',
|
| 1863 |
-
'photos': photos_list, 'colors': colors, 'models': models,
|
| 1864 |
'in_stock': in_stock, 'is_top': is_top
|
| 1865 |
}
|
| 1866 |
products.append(new_product)
|
| 1867 |
data['products'] = products
|
| 1868 |
save_data(data)
|
|
|
|
| 1869 |
flash(f"Товар '{name}' успешно добавлен.", 'success')
|
| 1870 |
-
|
| 1871 |
-
first_photo_filename = photos_list[0] if photos_list else None
|
| 1872 |
-
if bot_loop and bot_loop.is_running():
|
| 1873 |
-
coro = send_new_product_notification(name, price, first_photo_filename)
|
| 1874 |
-
asyncio.run_coroutine_threadsafe(coro, bot_loop)
|
| 1875 |
-
|
| 1876 |
|
| 1877 |
elif action == 'edit_product':
|
| 1878 |
product_id = request.form.get('product_id')
|
|
@@ -1888,6 +1938,7 @@ def admin():
|
|
| 1888 |
|
| 1889 |
if product_index == -1:
|
| 1890 |
flash(f"Ошибка редактирования: товар с ID '{product_id}' не найден.", 'error')
|
|
|
|
| 1891 |
return redirect(url_for('admin'))
|
| 1892 |
|
| 1893 |
product_to_edit = products[product_index]
|
|
@@ -1899,7 +1950,7 @@ def admin():
|
|
| 1899 |
category = request.form.get('category')
|
| 1900 |
product_to_edit['category'] = category if category in categories else 'Без категории'
|
| 1901 |
product_to_edit['colors'] = [c.strip() for c in request.form.getlist('colors') if c.strip()]
|
| 1902 |
-
product_to_edit['models'] = [m.strip() for m in request.form.getlist('models') if m.strip()]
|
| 1903 |
product_to_edit['in_stock'] = 'in_stock' in request.form
|
| 1904 |
product_to_edit['is_top'] = 'is_top' in request.form
|
| 1905 |
|
|
@@ -1908,6 +1959,7 @@ def admin():
|
|
| 1908 |
if price < 0: price = 0
|
| 1909 |
product_to_edit['price'] = price
|
| 1910 |
except ValueError:
|
|
|
|
| 1911 |
flash(f"Неверный формат цены для товара '{original_name}'. Цена не изменена.", 'warning')
|
| 1912 |
|
| 1913 |
photos_files = request.files.getlist('photos')
|
|
@@ -1918,14 +1970,17 @@ def admin():
|
|
| 1918 |
new_photos_list = []
|
| 1919 |
photo_limit = 10
|
| 1920 |
uploaded_count = 0
|
|
|
|
| 1921 |
for photo in photos_files:
|
| 1922 |
if uploaded_count >= photo_limit:
|
|
|
|
| 1923 |
flash(f"Загружено только первые {photo_limit} фото.", "warning")
|
| 1924 |
break
|
| 1925 |
if photo and photo.filename:
|
| 1926 |
try:
|
| 1927 |
ext = os.path.splitext(photo.filename)[1].lower()
|
| 1928 |
if ext not in ['.jpg', '.jpeg', '.png', '.gif', '.webp']:
|
|
|
|
| 1929 |
flash(f"Файл {photo.filename} не является изображением и был пропущен.", "warning")
|
| 1930 |
continue
|
| 1931 |
|
|
@@ -1933,13 +1988,16 @@ def admin():
|
|
| 1933 |
photo_filename = f"{safe_name}_{datetime.now().strftime('%Y%m%d%H%M%S%f')}{ext}"
|
| 1934 |
temp_path = os.path.join(uploads_dir, photo_filename)
|
| 1935 |
photo.save(temp_path)
|
|
|
|
| 1936 |
api.upload_file(path_or_fileobj=temp_path, path_in_repo=f"photos/{photo_filename}",
|
| 1937 |
repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE,
|
| 1938 |
commit_message=f"Update photo for product {product_to_edit['name']}")
|
| 1939 |
new_photos_list.append(photo_filename)
|
|
|
|
| 1940 |
os.remove(temp_path)
|
| 1941 |
uploaded_count += 1
|
| 1942 |
-
except Exception:
|
|
|
|
| 1943 |
flash(f"Ошибка при загрузке нового фото {photo.filename}.", 'error')
|
| 1944 |
if os.path.exists(temp_path):
|
| 1945 |
try: os.remove(temp_path)
|
|
@@ -1947,12 +2005,14 @@ def admin():
|
|
| 1947 |
try:
|
| 1948 |
if os.path.exists(uploads_dir) and not os.listdir(uploads_dir):
|
| 1949 |
os.rmdir(uploads_dir)
|
| 1950 |
-
except OSError:
|
| 1951 |
-
|
| 1952 |
|
| 1953 |
if new_photos_list:
|
|
|
|
| 1954 |
old_photos = product_to_edit.get('photos', [])
|
| 1955 |
if old_photos:
|
|
|
|
| 1956 |
try:
|
| 1957 |
api.delete_files(
|
| 1958 |
repo_id=REPO_ID,
|
|
@@ -1961,7 +2021,9 @@ def admin():
|
|
| 1961 |
token=HF_TOKEN_WRITE,
|
| 1962 |
commit_message=f"Delete old photos for product {product_to_edit['name']}"
|
| 1963 |
)
|
| 1964 |
-
|
|
|
|
|
|
|
| 1965 |
flash("Не удалось удалить старые фотографии с сервера. Новые фото загружены.", "warning")
|
| 1966 |
product_to_edit['photos'] = new_photos_list
|
| 1967 |
flash("Фотографии товара успешно обновлены.", "success")
|
|
@@ -1973,6 +2035,7 @@ def admin():
|
|
| 1973 |
products[product_index] = product_to_edit
|
| 1974 |
data['products'] = products
|
| 1975 |
save_data(data)
|
|
|
|
| 1976 |
flash(f"Товар '{product_to_edit['name']}' успешно обновлен.", 'success')
|
| 1977 |
|
| 1978 |
elif action == 'delete_product':
|
|
@@ -1993,6 +2056,7 @@ def admin():
|
|
| 1993 |
|
| 1994 |
photos_to_delete = product_to_delete.get('photos', [])
|
| 1995 |
if photos_to_delete and HF_TOKEN_WRITE:
|
|
|
|
| 1996 |
try:
|
| 1997 |
api = HfApi()
|
| 1998 |
api.delete_files(
|
|
@@ -2002,23 +2066,30 @@ def admin():
|
|
| 2002 |
token=HF_TOKEN_WRITE,
|
| 2003 |
commit_message=f"Delete photos for deleted product {product_name}"
|
| 2004 |
)
|
| 2005 |
-
|
|
|
|
|
|
|
| 2006 |
flash(f"Не удалось удалить фото для товара '{product_name}' с сервера. Товар удален локально.", "warning")
|
| 2007 |
elif photos_to_delete and not HF_TOKEN_WRITE:
|
|
|
|
| 2008 |
flash(f"Товар '{product_name}' удален локально, но фото не удалены с сервера (токен не задан).", "warning")
|
| 2009 |
|
| 2010 |
data['products'] = products
|
| 2011 |
save_data(data)
|
|
|
|
| 2012 |
flash(f"Товар '{product_name}' удален.", 'success')
|
| 2013 |
else:
|
| 2014 |
flash(f"Ошибка удаления: неверный ID товара '{product_id}'.", 'error')
|
|
|
|
| 2015 |
|
| 2016 |
else:
|
|
|
|
| 2017 |
flash(f"Неизвестное действие: {action}", 'warning')
|
| 2018 |
|
| 2019 |
return redirect(url_for('admin'))
|
| 2020 |
|
| 2021 |
-
except Exception:
|
|
|
|
| 2022 |
flash(f"Произошла внутренняя ошибка при выполнении действия '{action}'. Подробности в логе сервера.", 'error')
|
| 2023 |
return redirect(url_for('admin'))
|
| 2024 |
|
|
@@ -2027,6 +2098,7 @@ def admin():
|
|
| 2027 |
display_products = sorted(current_data.get('products', []), key=lambda p: p.get('name', '').lower())
|
| 2028 |
display_categories = sorted(current_data.get('categories', []))
|
| 2029 |
display_orders = list(current_data.get('orders', {}).values())
|
|
|
|
| 2030 |
|
| 2031 |
return render_template_string(
|
| 2032 |
ADMIN_TEMPLATE,
|
|
@@ -2037,19 +2109,30 @@ def admin():
|
|
| 2037 |
repo_id=REPO_ID,
|
| 2038 |
store_address=STORE_ADDRESS,
|
| 2039 |
currency_code=CURRENCY_CODE,
|
| 2040 |
-
currency_name=CURRENCY_NAME
|
|
|
|
| 2041 |
)
|
| 2042 |
|
| 2043 |
if __name__ == '__main__':
|
|
|
|
| 2044 |
download_db_from_hf()
|
| 2045 |
load_data()
|
|
|
|
| 2046 |
|
| 2047 |
if HF_TOKEN_WRITE:
|
| 2048 |
backup_thread = threading.Thread(target=periodic_backup, daemon=True)
|
| 2049 |
backup_thread.start()
|
|
|
|
|
|
|
|
|
|
| 2050 |
|
| 2051 |
-
|
| 2052 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2053 |
|
| 2054 |
port = int(os.environ.get('PORT', 7860))
|
|
|
|
| 2055 |
app.run(debug=False, host='0.0.0.0', port=port)
|
|
|
|
| 5 |
import logging
|
| 6 |
import threading
|
| 7 |
import time
|
| 8 |
+
import asyncio
|
| 9 |
from datetime import datetime
|
| 10 |
from huggingface_hub import HfApi, hf_hub_download
|
| 11 |
from huggingface_hub.utils import RepositoryNotFoundError, HfHubHTTPError
|
|
|
|
| 13 |
from dotenv import load_dotenv
|
| 14 |
import requests
|
| 15 |
import uuid
|
| 16 |
+
from aiogram import Bot, Dispatcher, types, F
|
|
|
|
|
|
|
|
|
|
| 17 |
from aiogram.filters import CommandStart
|
| 18 |
+
from aiogram.types import Message
|
|
|
|
|
|
|
| 19 |
|
| 20 |
load_dotenv()
|
| 21 |
|
|
|
|
| 28 |
REPO_ID = "Kgshop/aiocult"
|
| 29 |
HF_TOKEN_WRITE = os.getenv("HF_TOKEN")
|
| 30 |
HF_TOKEN_READ = os.getenv("HF_TOKEN_READ")
|
| 31 |
+
TELEGRAM_BOT_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN")
|
|
|
|
|
|
|
| 32 |
|
| 33 |
STORE_ADDRESS = " Алматы, «Байсат» 1 этаж, синий сектор 117-118 Бутик "
|
| 34 |
|
|
|
|
| 47 |
|
| 48 |
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
| 49 |
|
| 50 |
+
data_lock = threading.Lock()
|
| 51 |
+
|
| 52 |
+
bot = None
|
| 53 |
+
dp = None
|
| 54 |
+
|
| 55 |
+
if TELEGRAM_BOT_TOKEN:
|
| 56 |
+
bot = Bot(token=TELEGRAM_BOT_TOKEN)
|
| 57 |
+
dp = Dispatcher()
|
| 58 |
+
|
| 59 |
+
@dp.message(CommandStart())
|
| 60 |
+
async def command_start_handler(message: Message):
|
| 61 |
+
user_id = message.from_user.id
|
| 62 |
+
username = message.from_user.username
|
| 63 |
+
first_name = message.from_user.first_name
|
| 64 |
+
|
| 65 |
+
with data_lock:
|
| 66 |
+
data = load_data_internal()
|
| 67 |
+
if 'telegram_users' not in data:
|
| 68 |
+
data['telegram_users'] = {}
|
| 69 |
+
|
| 70 |
+
str_id = str(user_id)
|
| 71 |
+
if str_id not in data['telegram_users']:
|
| 72 |
+
data['telegram_users'][str_id] = {
|
| 73 |
+
'username': username,
|
| 74 |
+
'first_name': first_name,
|
| 75 |
+
'joined_at': datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
| 76 |
+
}
|
| 77 |
+
save_data_internal(data)
|
| 78 |
+
logging.info(f"New Telegram user registered: {user_id}")
|
| 79 |
+
|
| 80 |
+
await message.answer(f"Добро пожаловать в Мобильный мир, {first_name}! Здесь вы будете получать обновления о новинках.")
|
| 81 |
|
| 82 |
def download_db_from_hf(specific_file=None, retries=DOWNLOAD_RETRIES, delay=DOWNLOAD_DELAY):
|
| 83 |
+
if not HF_TOKEN_READ and not HF_TOKEN_WRITE:
|
| 84 |
+
logging.warning("HF_TOKEN_READ/HF_TOKEN_WRITE not set. Download might fail for private repos.")
|
| 85 |
+
|
| 86 |
token_to_use = HF_TOKEN_READ if HF_TOKEN_READ else HF_TOKEN_WRITE
|
| 87 |
|
| 88 |
files_to_download = [specific_file] if specific_file else SYNC_FILES
|
| 89 |
+
logging.info(f"Attempting download for {files_to_download} from {REPO_ID}...")
|
| 90 |
all_successful = True
|
| 91 |
|
| 92 |
for file_name in files_to_download:
|
| 93 |
success = False
|
| 94 |
for attempt in range(retries + 1):
|
| 95 |
try:
|
| 96 |
+
logging.info(f"Downloading {file_name} (Attempt {attempt + 1}/{retries + 1})...")
|
| 97 |
+
local_path = hf_hub_download(
|
| 98 |
repo_id=REPO_ID,
|
| 99 |
filename=file_name,
|
| 100 |
repo_type="dataset",
|
|
|
|
| 104 |
force_download=True,
|
| 105 |
resume_download=False
|
| 106 |
)
|
| 107 |
+
logging.info(f"Successfully downloaded {file_name} to {local_path}.")
|
| 108 |
success = True
|
| 109 |
break
|
| 110 |
except RepositoryNotFoundError:
|
| 111 |
+
logging.error(f"Repository {REPO_ID} not found. Download cancelled for all files.")
|
| 112 |
return False
|
| 113 |
except HfHubHTTPError as e:
|
| 114 |
if e.response.status_code == 404:
|
| 115 |
+
logging.warning(f"File {file_name} not found in repo {REPO_ID} (404). Skipping this file.")
|
| 116 |
if attempt == 0 and not os.path.exists(file_name):
|
| 117 |
try:
|
| 118 |
if file_name == DATA_FILE:
|
| 119 |
with open(file_name, 'w', encoding='utf-8') as f:
|
| 120 |
+
json.dump({'products': [], 'categories': [], 'orders': {}, 'telegram_users': {}}, f)
|
| 121 |
+
logging.info(f"Created empty local file {file_name} because it was not found on HF.")
|
| 122 |
+
except Exception as create_e:
|
| 123 |
+
logging.error(f"Failed to create empty local file {file_name}: {create_e}")
|
| 124 |
success = False
|
| 125 |
break
|
| 126 |
+
else:
|
| 127 |
+
logging.error(f"HTTP error downloading {file_name} (Attempt {attempt + 1}): {e}. Retrying in {delay}s...")
|
| 128 |
+
except requests.exceptions.RequestException as e:
|
| 129 |
+
logging.error(f"Network error downloading {file_name} (Attempt {attempt + 1}): {e}. Retrying in {delay}s...")
|
| 130 |
+
except Exception as e:
|
| 131 |
+
logging.error(f"Unexpected error downloading {file_name} (Attempt {attempt + 1}): {e}. Retrying in {delay}s...", exc_info=True)
|
| 132 |
|
| 133 |
if attempt < retries:
|
| 134 |
time.sleep(delay)
|
| 135 |
|
| 136 |
if not success:
|
| 137 |
+
logging.error(f"Failed to download {file_name} after {retries + 1} attempts.")
|
| 138 |
all_successful = False
|
| 139 |
|
| 140 |
+
logging.info(f"Download process finished. Overall success: {all_successful}")
|
| 141 |
return all_successful
|
| 142 |
|
| 143 |
def upload_db_to_hf(specific_file=None):
|
| 144 |
if not HF_TOKEN_WRITE:
|
| 145 |
+
logging.warning("HF_TOKEN (for writing) not set. Skipping upload to Hugging Face.")
|
| 146 |
return
|
| 147 |
try:
|
| 148 |
api = HfApi()
|
| 149 |
files_to_upload = [specific_file] if specific_file else SYNC_FILES
|
| 150 |
+
logging.info(f"Starting upload of {files_to_upload} to HF repo {REPO_ID}...")
|
| 151 |
|
| 152 |
for file_name in files_to_upload:
|
| 153 |
if os.path.exists(file_name):
|
|
|
|
| 160 |
token=HF_TOKEN_WRITE,
|
| 161 |
commit_message=f"Sync {file_name} {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
|
| 162 |
)
|
| 163 |
+
logging.info(f"File {file_name} successfully uploaded to Hugging Face.")
|
| 164 |
+
except Exception as e:
|
| 165 |
+
logging.error(f"Error uploading file {file_name} to Hugging Face: {e}")
|
| 166 |
+
else:
|
| 167 |
+
logging.warning(f"File {file_name} not found locally, skipping upload.")
|
| 168 |
+
logging.info("Finished uploading files to HF.")
|
| 169 |
+
except Exception as e:
|
| 170 |
+
logging.error(f"General error during Hugging Face upload initialization or process: {e}", exc_info=True)
|
| 171 |
|
| 172 |
def periodic_backup():
|
| 173 |
backup_interval = 1800
|
| 174 |
+
logging.info(f"Setting up periodic backup every {backup_interval} seconds.")
|
| 175 |
while True:
|
| 176 |
time.sleep(backup_interval)
|
| 177 |
+
logging.info("Starting periodic backup...")
|
| 178 |
upload_db_to_hf()
|
| 179 |
+
logging.info("Periodic backup finished.")
|
| 180 |
|
| 181 |
+
def load_data_internal():
|
| 182 |
+
default_data = {'products': [], 'categories': [], 'orders': {}, 'telegram_users': {}}
|
| 183 |
try:
|
| 184 |
with open(DATA_FILE, 'r', encoding='utf-8') as file:
|
| 185 |
data = json.load(file)
|
|
|
|
| 188 |
if 'products' not in data: data['products'] = []
|
| 189 |
if 'categories' not in data: data['categories'] = []
|
| 190 |
if 'orders' not in data: data['orders'] = {}
|
| 191 |
+
if 'telegram_users' not in data: data['telegram_users'] = {}
|
| 192 |
return data
|
| 193 |
except (FileNotFoundError, json.JSONDecodeError):
|
| 194 |
+
return default_data
|
| 195 |
|
| 196 |
+
def load_data():
|
| 197 |
+
with data_lock:
|
| 198 |
+
default_data = {'products': [], 'categories': [], 'orders': {}, 'telegram_users': {}}
|
| 199 |
try:
|
| 200 |
with open(DATA_FILE, 'r', encoding='utf-8') as file:
|
| 201 |
data = json.load(file)
|
| 202 |
+
logging.info(f"Local data loaded successfully from {DATA_FILE}")
|
| 203 |
if not isinstance(data, dict):
|
| 204 |
+
logging.warning(f"Local {DATA_FILE} is not a dictionary. Attempting download.")
|
| 205 |
+
raise FileNotFoundError
|
| 206 |
if 'products' not in data: data['products'] = []
|
| 207 |
if 'categories' not in data: data['categories'] = []
|
| 208 |
if 'orders' not in data: data['orders'] = {}
|
| 209 |
+
if 'telegram_users' not in data: data['telegram_users'] = {}
|
| 210 |
return data
|
| 211 |
+
except FileNotFoundError:
|
| 212 |
+
logging.warning(f"Local file {DATA_FILE} not found. Attempting download from HF.")
|
| 213 |
+
except json.JSONDecodeError:
|
| 214 |
+
logging.error(f"Error decoding JSON in local {DATA_FILE}. File might be corrupt. Attempting download.")
|
| 215 |
+
|
| 216 |
+
if download_db_from_hf(specific_file=DATA_FILE):
|
| 217 |
try:
|
| 218 |
+
with open(DATA_FILE, 'r', encoding='utf-8') as file:
|
| 219 |
+
data = json.load(file)
|
| 220 |
+
logging.info(f"Data loaded successfully from {DATA_FILE} after download.")
|
| 221 |
+
if not isinstance(data, dict):
|
| 222 |
+
return default_data
|
| 223 |
+
if 'products' not in data: data['products'] = []
|
| 224 |
+
if 'categories' not in data: data['categories'] = []
|
| 225 |
+
if 'orders' not in data: data['orders'] = {}
|
| 226 |
+
if 'telegram_users' not in data: data['telegram_users'] = {}
|
| 227 |
+
return data
|
| 228 |
+
except Exception as e:
|
| 229 |
+
logging.error(f"Unknown error loading downloaded {DATA_FILE}: {e}. Using default.", exc_info=True)
|
| 230 |
+
return default_data
|
| 231 |
+
else:
|
| 232 |
+
logging.error(f"Failed to download {DATA_FILE} from HF after retries. Using empty default data structure.")
|
| 233 |
+
if not os.path.exists(DATA_FILE):
|
| 234 |
+
try:
|
| 235 |
+
with open(DATA_FILE, 'w', encoding='utf-8') as f:
|
| 236 |
+
json.dump(default_data, f)
|
| 237 |
+
logging.info(f"Created empty local file {DATA_FILE} after failed download.")
|
| 238 |
+
except Exception as create_e:
|
| 239 |
+
logging.error(f"Failed to create empty local file {DATA_FILE}: {create_e}")
|
| 240 |
+
return default_data
|
| 241 |
|
| 242 |
+
def save_data_internal(data):
|
| 243 |
try:
|
|
|
|
|
|
|
| 244 |
if 'products' not in data: data['products'] = []
|
| 245 |
if 'categories' not in data: data['categories'] = []
|
| 246 |
if 'orders' not in data: data['orders'] = {}
|
| 247 |
+
if 'telegram_users' not in data: data['telegram_users'] = {}
|
| 248 |
|
| 249 |
with open(DATA_FILE, 'w', encoding='utf-8') as file:
|
| 250 |
json.dump(data, file, ensure_ascii=False, indent=4)
|
| 251 |
upload_db_to_hf(specific_file=DATA_FILE)
|
| 252 |
+
except Exception as e:
|
| 253 |
+
logging.error(f"Error saving data to {DATA_FILE}: {e}", exc_info=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 254 |
|
| 255 |
+
def save_data(data):
|
| 256 |
+
with data_lock:
|
| 257 |
+
save_data_internal(data)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 258 |
|
| 259 |
+
def start_bot_polling():
|
| 260 |
+
if bot and dp:
|
| 261 |
+
logging.info("Starting Aiogram bot polling...")
|
| 262 |
+
asyncio.run(dp.start_polling(bot))
|
| 263 |
|
| 264 |
+
async def broadcast_message(user_ids, text):
|
| 265 |
+
count = 0
|
| 266 |
+
for user_id in user_ids:
|
| 267 |
try:
|
| 268 |
+
await bot.send_message(chat_id=user_id, text=text)
|
| 269 |
+
count += 1
|
| 270 |
+
await asyncio.sleep(0.05)
|
| 271 |
except Exception as e:
|
| 272 |
+
logging.error(f"Failed to send message to {user_id}: {e}")
|
| 273 |
+
return count
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 274 |
|
| 275 |
CATALOG_TEMPLATE = '''
|
| 276 |
<!DOCTYPE html>
|
|
|
|
| 282 |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
|
| 283 |
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap" rel="stylesheet">
|
| 284 |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/Swiper/10.2.0/swiper-bundle.min.css">
|
|
|
|
| 285 |
<style>
|
| 286 |
:root {
|
| 287 |
+
--primary-color: #007AFF;
|
| 288 |
--primary-dark: #0056b3;
|
| 289 |
--surface-color: #FFFFFF;
|
| 290 |
--surface-dark: #21252B;
|
|
|
|
| 309 |
.theme-toggle { background: none; border: none; font-size: 1.3rem; cursor: pointer; color: var(--primary-color); padding: 5px; }
|
| 310 |
.theme-toggle:hover { opacity: 0.8; }
|
| 311 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 312 |
.store-address { padding: 12px 15px; text-align: center; background-color: var(--surface-color); border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.05); font-size: 0.9rem; color: #555; margin: 15px; }
|
| 313 |
body.dark-mode .store-address { background-color: var(--surface-dark); color: var(--text-color-dark); box-shadow: 0 1px 3px rgba(255,255,255,0.1); }
|
| 314 |
|
|
|
|
| 336 |
.product h2 { font-size: 0.95rem; font-weight: 500; margin: 0 0 5px 0; text-align: center; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: var(--text-color); }
|
| 337 |
body.dark-mode .product h2 { color: var(--text-color-dark); }
|
| 338 |
.product-price { font-size: 1.1rem; color: var(--primary-color); font-weight: 700; text-align: center; margin: 5px 0; }
|
| 339 |
+
.product-description { display: none; }
|
| 340 |
.product-actions { padding: 0 10px 10px 10px; display: flex; flex-direction: column; gap: 6px; }
|
| 341 |
|
| 342 |
.product-button { display: block; width: 100%; padding: 8px; border: none; border-radius: 8px; background-color: var(--primary-color); color: white; font-size: 0.85rem; font-weight: 500; cursor: pointer; transition: all 0.2s ease; text-align: center; text-decoration: none; }
|
|
|
|
| 394 |
<div class="logo-title-container" style="display: flex; align-items: center; gap: 10px;">
|
| 395 |
<img src="https://huggingface.co/spaces/Kgshop/mobilnymir/resolve/main/IMG_20251019_120056_121.jpg" alt="Мобильный мир Logo">
|
| 396 |
<h1>Мобильный мир</h1>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 397 |
</div>
|
| 398 |
<button class="theme-toggle" onclick="toggleTheme()" aria-label="Переключить тему">
|
| 399 |
<i class="fas fa-moon"></i>
|
|
|
|
| 651 |
return;
|
| 652 |
}
|
| 653 |
|
| 654 |
+
const cartItemId = `${product.name}-${color}-${model}`;
|
| 655 |
const existingItemIndex = cart.findIndex(item => item.id === cartItemId);
|
| 656 |
|
| 657 |
if (existingItemIndex > -1) {
|
|
|
|
| 664 |
photo: product.photos && product.photos.length > 0 ? product.photos[0] : null,
|
| 665 |
quantity: quantity,
|
| 666 |
color: color,
|
| 667 |
+
model: model
|
| 668 |
});
|
| 669 |
}
|
| 670 |
|
|
|
|
| 711 |
|
| 712 |
let variantText = [];
|
| 713 |
if (item.color !== 'N/A') variantText.push(`Цвет: ${item.color}`);
|
| 714 |
+
if (item.model !== 'N/A') variantText.push(`Модель: ${item.model}`);
|
| 715 |
|
| 716 |
const variantDisplay = variantText.length > 0 ? ` (${variantText.join(', ')})` : '';
|
| 717 |
|
|
|
|
| 808 |
if (existingNoResults) existingNoResults.remove();
|
| 809 |
|
| 810 |
const allProductElements = document.querySelectorAll('.products-grid .product');
|
| 811 |
+
if (allProductElements.length === 0 && products.length > 0) return;
|
| 812 |
|
| 813 |
document.querySelectorAll('.products-grid .product').forEach(productElement => {
|
| 814 |
const name = productElement.getAttribute('data-name');
|
|
|
|
| 886 |
notification.addEventListener('transitionend', () => notification.remove());
|
| 887 |
}, duration);
|
| 888 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 889 |
|
| 890 |
document.addEventListener('DOMContentLoaded', () => {
|
| 891 |
applyInitialTheme();
|
| 892 |
updateCartButton();
|
| 893 |
setupFilters();
|
|
|
|
| 894 |
|
| 895 |
window.addEventListener('click', function(event) {
|
| 896 |
if (event.target.classList.contains('modal')) {
|
|
|
|
| 1094 |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
|
| 1095 |
<style>
|
| 1096 |
:root {
|
| 1097 |
+
--primary-color: #007AFF;
|
| 1098 |
--primary-dark: #0056b3;
|
| 1099 |
--surface-color: #FFFFFF;
|
| 1100 |
--surface-dark: #21252B;
|
|
|
|
| 1168 |
.status-indicator.out-of-stock { background-color: #fed7d7; color: #c53030; }
|
| 1169 |
.status-indicator.top-product { background-color: #fff3c4; color: #9c4221; margin-left: 5px;}
|
| 1170 |
|
| 1171 |
+
.status-indicator.new { background-color: #ffe0b2; color: #e65100; }
|
| 1172 |
+
.status-indicator.accepted { background-color: #b3e5fc; color: #0277bd; }
|
| 1173 |
+
.status-indicator.prepared { background-color: #c6f6d5; color: #2f855a; }
|
| 1174 |
+
.status-indicator.shipped { background-color: #a7f3d0; color: #065f46; }
|
| 1175 |
|
| 1176 |
details { background-color: var(--surface-color); border: 1px solid var(--border-color); border-radius: 8px; margin-bottom: 10px; }
|
| 1177 |
body.dark-mode details { background-color: var(--surface-dark); border-color: var(--border-color-dark); }
|
|
|
|
| 1223 |
{% endfor %}
|
| 1224 |
{% endif %}
|
| 1225 |
{% endwith %}
|
| 1226 |
+
|
| 1227 |
+
<div class="section">
|
| 1228 |
+
<h2><i class="fab fa-telegram-plane"></i> Рассылка Telegram</h2>
|
| 1229 |
+
<details>
|
| 1230 |
+
<summary>Отправить сообщение всем пользователям бота</summary>
|
| 1231 |
+
<div class="form-content" style="padding: 15px;">
|
| 1232 |
+
<form method="POST">
|
| 1233 |
+
<input type="hidden" name="action" value="send_telegram_broadcast">
|
| 1234 |
+
<label for="broadcast_message">Текст сообщения:</label>
|
| 1235 |
+
<textarea id="broadcast_message" name="message" rows="4" required></textarea>
|
| 1236 |
+
<p style="font-size: 0.85rem; color: #666; margin-top: 5px;">
|
| 1237 |
+
Сообщение будет отправлено {{ telegram_users_count }} пользователям.
|
| 1238 |
+
</p>
|
| 1239 |
+
<button type="submit" class="button"><i class="fas fa-paper-plane"></i> Отправить</button>
|
| 1240 |
+
</form>
|
| 1241 |
+
</div>
|
| 1242 |
+
</details>
|
| 1243 |
+
</div>
|
| 1244 |
|
| 1245 |
<div class="section">
|
| 1246 |
<h2><i class="fas fa-history"></i> История заказов</h2>
|
|
|
|
| 1572 |
if (container) {
|
| 1573 |
const existingInputs = container.querySelectorAll('input[name="' + fieldName + '"]');
|
| 1574 |
if (existingInputs.length === 1 && existingInputs[0].value.trim() === '') {
|
| 1575 |
+
existingInputs[0].value = '';
|
| 1576 |
}
|
| 1577 |
|
| 1578 |
const newInputGroup = document.createElement('div');
|
|
|
|
| 1642 |
try:
|
| 1643 |
product = products_sorted[index]
|
| 1644 |
except IndexError:
|
| 1645 |
+
logging.warning(f"Attempted access to non-existent or out-of-stock product with index {index}")
|
| 1646 |
return "Товар не найден или отсутствует в наличии.", 404
|
| 1647 |
|
| 1648 |
products_index = index
|
|
|
|
| 1660 |
order_data = request.get_json()
|
| 1661 |
|
| 1662 |
if not order_data or 'cart' not in order_data or not order_data['cart']:
|
| 1663 |
+
logging.warning("Create order request missing cart data or cart is empty.")
|
| 1664 |
return jsonify({"error": "Корзина пуста или не передана."}), 400
|
| 1665 |
|
| 1666 |
cart_items = order_data['cart']
|
|
|
|
| 1669 |
processed_cart = []
|
| 1670 |
for item in cart_items:
|
| 1671 |
if not all(k in item for k in ('name', 'price', 'quantity')):
|
| 1672 |
+
logging.error(f"Invalid cart item structure received: {item}")
|
| 1673 |
return jsonify({"error": "Неверный формат товара в корзине."}), 400
|
| 1674 |
try:
|
| 1675 |
price = float(item['price'])
|
|
|
|
| 1691 |
"photo_url": photo_url
|
| 1692 |
})
|
| 1693 |
except (ValueError, TypeError) as e:
|
| 1694 |
+
logging.error(f"Invalid price/quantity in cart item: {item}. Error: {e}")
|
| 1695 |
return jsonify({"error": "Неверная цена или количество в товаре."}), 400
|
| 1696 |
|
| 1697 |
order_id = f"{datetime.now().strftime('%Y%m%d%H%M%S')}-{uuid.uuid4().hex[:6]}"
|
|
|
|
| 1713 |
|
| 1714 |
data['orders'][order_id] = new_order
|
| 1715 |
save_data(data)
|
| 1716 |
+
logging.info(f"Order {order_id} created successfully (anonymously).")
|
| 1717 |
return jsonify({"order_id": order_id}), 201
|
| 1718 |
|
| 1719 |
except Exception as e:
|
| 1720 |
+
logging.error(f"Failed to save order {order_id}: {e}", exc_info=True)
|
| 1721 |
return jsonify({"error": "Ошибка сервера при сохранении заказа."}), 500
|
| 1722 |
|
| 1723 |
@app.route('/order/<order_id>')
|
|
|
|
| 1725 |
data = load_data()
|
| 1726 |
order = data.get('orders', {}).get(order_id)
|
| 1727 |
|
| 1728 |
+
if order:
|
| 1729 |
+
logging.info(f"Displaying order {order_id}")
|
| 1730 |
+
else:
|
| 1731 |
+
logging.warning(f"Order {order_id} not found.")
|
| 1732 |
+
|
| 1733 |
return render_template_string(ORDER_TEMPLATE,
|
| 1734 |
order=order,
|
| 1735 |
status_map_ru=STATUS_MAP_RU,
|
|
|
|
| 1742 |
products = data.get('products', [])
|
| 1743 |
categories = data.get('categories', [])
|
| 1744 |
orders = data.get('orders', {})
|
| 1745 |
+
telegram_users = data.get('telegram_users', {})
|
| 1746 |
|
| 1747 |
needs_save = False
|
| 1748 |
for product in products:
|
|
|
|
| 1750 |
product['id'] = str(uuid.uuid4())
|
| 1751 |
needs_save = True
|
| 1752 |
if needs_save:
|
| 1753 |
+
logging.info("Assigning unique IDs to products that were missing them.")
|
| 1754 |
data['products'] = products
|
| 1755 |
save_data(data)
|
| 1756 |
|
|
|
|
| 1757 |
if 'orders' not in data or not isinstance(data.get('orders'), dict):
|
| 1758 |
data['orders'] = {}
|
| 1759 |
|
| 1760 |
if request.method == 'POST':
|
| 1761 |
action = request.form.get('action')
|
| 1762 |
+
logging.info(f"Admin action received: {action}")
|
| 1763 |
|
| 1764 |
try:
|
| 1765 |
+
if action == 'send_telegram_broadcast':
|
| 1766 |
+
message_text = request.form.get('message', '').strip()
|
| 1767 |
+
if message_text:
|
| 1768 |
+
if telegram_users:
|
| 1769 |
+
user_ids = list(telegram_users.keys())
|
| 1770 |
+
|
| 1771 |
+
loop = asyncio.new_event_loop()
|
| 1772 |
+
asyncio.set_event_loop(loop)
|
| 1773 |
+
sent_count = loop.run_until_complete(broadcast_message(user_ids, message_text))
|
| 1774 |
+
loop.close()
|
| 1775 |
+
|
| 1776 |
+
flash(f"Сообщение отправлено {sent_count} из {len(user_ids)} пользователей.", 'success')
|
| 1777 |
+
else:
|
| 1778 |
+
flash("Нет зарегистрированных пользователей Telegram для рассылки.", 'warning')
|
| 1779 |
+
else:
|
| 1780 |
+
flash("Текст сообщения не может быть пустым.", 'error')
|
| 1781 |
+
|
| 1782 |
+
elif action == 'update_order_status':
|
| 1783 |
order_id = request.form.get('order_id')
|
| 1784 |
new_status = request.form.get('new_status')
|
| 1785 |
|
|
|
|
| 1788 |
valid_statuses = list(STATUS_MAP_RU.keys())
|
| 1789 |
|
| 1790 |
if new_status in valid_statuses:
|
| 1791 |
+
old_status_key = order.get('status', 'new')
|
| 1792 |
+
|
| 1793 |
order['status'] = new_status
|
| 1794 |
data['orders'][order_id] = order
|
| 1795 |
save_data(data)
|
| 1796 |
+
logging.info(f"Order {order_id} status updated from {old_status_key} to {new_status}")
|
| 1797 |
flash(f"Статус заказа №{order_id} успешно изменен на '{STATUS_MAP_RU.get(new_status)}'.", 'success')
|
| 1798 |
else:
|
| 1799 |
flash(f"Неверный статус: {new_status}", 'error')
|
|
|
|
| 1808 |
categories.append(category_name)
|
| 1809 |
data['categories'] = categories
|
| 1810 |
save_data(data)
|
| 1811 |
+
logging.info(f"Category '{category_name}' added.")
|
| 1812 |
flash(f"Категория '{category_name}' успешно добавлена.", 'success')
|
| 1813 |
elif not category_name:
|
| 1814 |
+
logging.warning("Attempted to add empty category.")
|
| 1815 |
flash("Название категории не может быть пустым.", 'error')
|
| 1816 |
else:
|
| 1817 |
+
logging.warning(f"Category '{category_name}' already exists.")
|
| 1818 |
flash(f"Категория '{category_name}' уже существует.", 'error')
|
| 1819 |
|
| 1820 |
elif action == 'delete_category':
|
|
|
|
| 1829 |
data['categories'] = categories
|
| 1830 |
data['products'] = products
|
| 1831 |
save_data(data)
|
| 1832 |
+
logging.info(f"Category '{category_to_delete}' deleted. Updated products: {updated_count}.")
|
| 1833 |
flash(f"Категория '{category_to_delete}' удалена. {updated_count} товаров обновлено.", 'success')
|
| 1834 |
else:
|
| 1835 |
+
logging.warning(f"Attempted to delete non-existent or empty category: {category_to_delete}")
|
| 1836 |
flash(f"Не удалось удалить категорию '{category_to_delete}'.", 'error')
|
| 1837 |
|
| 1838 |
elif action == 'add_product':
|
|
|
|
| 1842 |
category = request.form.get('category')
|
| 1843 |
photos_files = request.files.getlist('photos')
|
| 1844 |
colors = [c.strip() for c in request.form.getlist('colors') if c.strip()]
|
| 1845 |
+
models = [m.strip() for m in request.form.getlist('models') if m.strip()]
|
| 1846 |
in_stock = 'in_stock' in request.form
|
| 1847 |
is_top = 'is_top' in request.form
|
| 1848 |
|
|
|
|
| 1866 |
uploaded_count = 0
|
| 1867 |
for photo in photos_files:
|
| 1868 |
if uploaded_count >= photo_limit:
|
| 1869 |
+
logging.warning(f"Photo limit ({photo_limit}) reached, ignoring remaining photos.")
|
| 1870 |
flash(f"Загружено только первые {photo_limit} фото.", "warning")
|
| 1871 |
break
|
| 1872 |
if photo and photo.filename:
|
| 1873 |
try:
|
| 1874 |
ext = os.path.splitext(photo.filename)[1].lower()
|
| 1875 |
if ext not in ['.jpg', '.jpeg', '.png', '.gif', '.webp']:
|
| 1876 |
+
logging.warning(f"Skipping non-image file upload: {photo.filename}")
|
| 1877 |
flash(f"Файл {photo.filename} не является изображением и был пропущен.", "warning")
|
| 1878 |
continue
|
| 1879 |
|
|
|
|
| 1881 |
photo_filename = f"{safe_name}_{datetime.now().strftime('%Y%m%d%H%M%S%f')}{ext}"
|
| 1882 |
temp_path = os.path.join(uploads_dir, photo_filename)
|
| 1883 |
photo.save(temp_path)
|
| 1884 |
+
logging.info(f"Uploading photo {photo_filename} to HF for product {name}...")
|
| 1885 |
api.upload_file(
|
| 1886 |
path_or_fileobj=temp_path,
|
| 1887 |
path_in_repo=f"photos/{photo_filename}",
|
|
|
|
| 1891 |
commit_message=f"Add photo for product {name}"
|
| 1892 |
)
|
| 1893 |
photos_list.append(photo_filename)
|
| 1894 |
+
logging.info(f"Photo {photo_filename} uploaded successfully.")
|
| 1895 |
os.remove(temp_path)
|
| 1896 |
uploaded_count += 1
|
| 1897 |
+
except Exception as e:
|
| 1898 |
+
logging.error(f"Error uploading photo {photo.filename} to HF: {e}", exc_info=True)
|
| 1899 |
flash(f"Ошибка при загрузке фото {photo.filename}.", 'error')
|
| 1900 |
if os.path.exists(temp_path):
|
| 1901 |
try: os.remove(temp_path)
|
| 1902 |
except OSError: pass
|
| 1903 |
+
elif photo and not photo.filename:
|
| 1904 |
+
logging.warning("Received an empty photo file object when adding product.")
|
| 1905 |
try:
|
| 1906 |
if os.path.exists(uploads_dir) and not os.listdir(uploads_dir):
|
| 1907 |
os.rmdir(uploads_dir)
|
| 1908 |
+
except OSError as e:
|
| 1909 |
+
logging.warning(f"Could not remove temporary upload directory {uploads_dir}: {e}")
|
| 1910 |
elif not HF_TOKEN_WRITE and photos_files and any(f.filename for f in photos_files):
|
| 1911 |
flash("HF_TOKEN (write) не настроен. Фотографии не были загружены.", "warning")
|
| 1912 |
|
|
|
|
| 1915 |
'id': str(uuid.uuid4()),
|
| 1916 |
'name': name, 'price': price, 'description': description,
|
| 1917 |
'category': category if category in categories else 'Без категории',
|
| 1918 |
+
'photos': photos_list, 'colors': colors, 'models': models,
|
| 1919 |
'in_stock': in_stock, 'is_top': is_top
|
| 1920 |
}
|
| 1921 |
products.append(new_product)
|
| 1922 |
data['products'] = products
|
| 1923 |
save_data(data)
|
| 1924 |
+
logging.info(f"Product '{name}' added.")
|
| 1925 |
flash(f"Товар '{name}' успешно добавлен.", 'success')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1926 |
|
| 1927 |
elif action == 'edit_product':
|
| 1928 |
product_id = request.form.get('product_id')
|
|
|
|
| 1938 |
|
| 1939 |
if product_index == -1:
|
| 1940 |
flash(f"Ошибка редактирования: товар с ID '{product_id}' не найден.", 'error')
|
| 1941 |
+
logging.error(f"Invalid product ID '{product_id}' for editing.")
|
| 1942 |
return redirect(url_for('admin'))
|
| 1943 |
|
| 1944 |
product_to_edit = products[product_index]
|
|
|
|
| 1950 |
category = request.form.get('category')
|
| 1951 |
product_to_edit['category'] = category if category in categories else 'Без категории'
|
| 1952 |
product_to_edit['colors'] = [c.strip() for c in request.form.getlist('colors') if c.strip()]
|
| 1953 |
+
product_to_edit['models'] = [m.strip() for m in request.form.getlist('models') if m.strip()]
|
| 1954 |
product_to_edit['in_stock'] = 'in_stock' in request.form
|
| 1955 |
product_to_edit['is_top'] = 'is_top' in request.form
|
| 1956 |
|
|
|
|
| 1959 |
if price < 0: price = 0
|
| 1960 |
product_to_edit['price'] = price
|
| 1961 |
except ValueError:
|
| 1962 |
+
logging.warning(f"Invalid price format '{price_str}' during edit of product {original_name}. Price not changed.")
|
| 1963 |
flash(f"Неверный формат цены для товара '{original_name}'. Цена не изменена.", 'warning')
|
| 1964 |
|
| 1965 |
photos_files = request.files.getlist('photos')
|
|
|
|
| 1970 |
new_photos_list = []
|
| 1971 |
photo_limit = 10
|
| 1972 |
uploaded_count = 0
|
| 1973 |
+
logging.info(f"Uploading new photos for product {product_to_edit['name']}...")
|
| 1974 |
for photo in photos_files:
|
| 1975 |
if uploaded_count >= photo_limit:
|
| 1976 |
+
logging.warning(f"Photo limit ({photo_limit}) reached, ignoring remaining photos.")
|
| 1977 |
flash(f"Загружено только первые {photo_limit} фото.", "warning")
|
| 1978 |
break
|
| 1979 |
if photo and photo.filename:
|
| 1980 |
try:
|
| 1981 |
ext = os.path.splitext(photo.filename)[1].lower()
|
| 1982 |
if ext not in ['.jpg', '.jpeg', '.png', '.gif', '.webp']:
|
| 1983 |
+
logging.warning(f"Skipping non-image file upload during edit: {photo.filename}")
|
| 1984 |
flash(f"Файл {photo.filename} не является изображением и был пропущен.", "warning")
|
| 1985 |
continue
|
| 1986 |
|
|
|
|
| 1988 |
photo_filename = f"{safe_name}_{datetime.now().strftime('%Y%m%d%H%M%S%f')}{ext}"
|
| 1989 |
temp_path = os.path.join(uploads_dir, photo_filename)
|
| 1990 |
photo.save(temp_path)
|
| 1991 |
+
logging.info(f"Uploading new photo {photo_filename} to HF...")
|
| 1992 |
api.upload_file(path_or_fileobj=temp_path, path_in_repo=f"photos/{photo_filename}",
|
| 1993 |
repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE,
|
| 1994 |
commit_message=f"Update photo for product {product_to_edit['name']}")
|
| 1995 |
new_photos_list.append(photo_filename)
|
| 1996 |
+
logging.info(f"New photo {photo_filename} uploaded successfully.")
|
| 1997 |
os.remove(temp_path)
|
| 1998 |
uploaded_count += 1
|
| 1999 |
+
except Exception as e:
|
| 2000 |
+
logging.error(f"Error uploading new photo {photo.filename}: {e}", exc_info=True)
|
| 2001 |
flash(f"Ошибка при загрузке нового фото {photo.filename}.", 'error')
|
| 2002 |
if os.path.exists(temp_path):
|
| 2003 |
try: os.remove(temp_path)
|
|
|
|
| 2005 |
try:
|
| 2006 |
if os.path.exists(uploads_dir) and not os.listdir(uploads_dir):
|
| 2007 |
os.rmdir(uploads_dir)
|
| 2008 |
+
except OSError as e:
|
| 2009 |
+
logging.warning(f"Could not remove temporary upload directory {uploads_dir}: {e}")
|
| 2010 |
|
| 2011 |
if new_photos_list:
|
| 2012 |
+
logging.info(f"New photo list for product {product_to_edit['name']} generated.")
|
| 2013 |
old_photos = product_to_edit.get('photos', [])
|
| 2014 |
if old_photos:
|
| 2015 |
+
logging.info(f"Attempting to delete old photos: {old_photos}")
|
| 2016 |
try:
|
| 2017 |
api.delete_files(
|
| 2018 |
repo_id=REPO_ID,
|
|
|
|
| 2021 |
token=HF_TOKEN_WRITE,
|
| 2022 |
commit_message=f"Delete old photos for product {product_to_edit['name']}"
|
| 2023 |
)
|
| 2024 |
+
logging.info(f"Old photos for product {product_to_edit['name']} deleted from HF.")
|
| 2025 |
+
except Exception as e:
|
| 2026 |
+
logging.error(f"Error deleting old photos {old_photos} from HF: {e}", exc_info=True)
|
| 2027 |
flash("Не удалось удалить старые фотографии с сервера. Новые фото загружены.", "warning")
|
| 2028 |
product_to_edit['photos'] = new_photos_list
|
| 2029 |
flash("Фотографии товара успешно обновлены.", "success")
|
|
|
|
| 2035 |
products[product_index] = product_to_edit
|
| 2036 |
data['products'] = products
|
| 2037 |
save_data(data)
|
| 2038 |
+
logging.info(f"Product '{original_name}' (ID {product_id}) updated to '{product_to_edit['name']}'.")
|
| 2039 |
flash(f"Товар '{product_to_edit['name']}' успешно обновлен.", 'success')
|
| 2040 |
|
| 2041 |
elif action == 'delete_product':
|
|
|
|
| 2056 |
|
| 2057 |
photos_to_delete = product_to_delete.get('photos', [])
|
| 2058 |
if photos_to_delete and HF_TOKEN_WRITE:
|
| 2059 |
+
logging.info(f"Attempting to delete photos for product '{product_name}' from HF: {photos_to_delete}")
|
| 2060 |
try:
|
| 2061 |
api = HfApi()
|
| 2062 |
api.delete_files(
|
|
|
|
| 2066 |
token=HF_TOKEN_WRITE,
|
| 2067 |
commit_message=f"Delete photos for deleted product {product_name}"
|
| 2068 |
)
|
| 2069 |
+
logging.info(f"Photos for product '{product_name}' deleted from HF.")
|
| 2070 |
+
except Exception as e:
|
| 2071 |
+
logging.error(f"Error deleting photos {photos_to_delete} for product '{product_name}' from HF: {e}", exc_info=True)
|
| 2072 |
flash(f"Не удалось удалить фото для товара '{product_name}' с сервера. Товар удален локально.", "warning")
|
| 2073 |
elif photos_to_delete and not HF_TOKEN_WRITE:
|
| 2074 |
+
logging.warning(f"HF_TOKEN (write) not set. Cannot delete photos {photos_to_delete} for deleted product '{product_name}'.")
|
| 2075 |
flash(f"Товар '{product_name}' удален локально, но фото не удалены с сервера (токен не задан).", "warning")
|
| 2076 |
|
| 2077 |
data['products'] = products
|
| 2078 |
save_data(data)
|
| 2079 |
+
logging.info(f"Product '{product_name}' (ID {product_id}) deleted.")
|
| 2080 |
flash(f"Товар '{product_name}' удален.", 'success')
|
| 2081 |
else:
|
| 2082 |
flash(f"Ошибка удаления: неверный ID товара '{product_id}'.", 'error')
|
| 2083 |
+
logging.error(f"Invalid product ID '{product_id}' for deletion.")
|
| 2084 |
|
| 2085 |
else:
|
| 2086 |
+
logging.warning(f"Received unknown admin action: {action}")
|
| 2087 |
flash(f"Неизвестное действие: {action}", 'warning')
|
| 2088 |
|
| 2089 |
return redirect(url_for('admin'))
|
| 2090 |
|
| 2091 |
+
except Exception as e:
|
| 2092 |
+
logging.error(f"Error processing admin action '{action}': {e}", exc_info=True)
|
| 2093 |
flash(f"Произошла внутренняя ошибка при выполнении действия '{action}'. Подробности в логе сервера.", 'error')
|
| 2094 |
return redirect(url_for('admin'))
|
| 2095 |
|
|
|
|
| 2098 |
display_products = sorted(current_data.get('products', []), key=lambda p: p.get('name', '').lower())
|
| 2099 |
display_categories = sorted(current_data.get('categories', []))
|
| 2100 |
display_orders = list(current_data.get('orders', {}).values())
|
| 2101 |
+
telegram_users_count = len(current_data.get('telegram_users', {}))
|
| 2102 |
|
| 2103 |
return render_template_string(
|
| 2104 |
ADMIN_TEMPLATE,
|
|
|
|
| 2109 |
repo_id=REPO_ID,
|
| 2110 |
store_address=STORE_ADDRESS,
|
| 2111 |
currency_code=CURRENCY_CODE,
|
| 2112 |
+
currency_name=CURRENCY_NAME,
|
| 2113 |
+
telegram_users_count=telegram_users_count
|
| 2114 |
)
|
| 2115 |
|
| 2116 |
if __name__ == '__main__':
|
| 2117 |
+
logging.info("Application starting up. Performing initial data load/download...")
|
| 2118 |
download_db_from_hf()
|
| 2119 |
load_data()
|
| 2120 |
+
logging.info("Initial data load complete.")
|
| 2121 |
|
| 2122 |
if HF_TOKEN_WRITE:
|
| 2123 |
backup_thread = threading.Thread(target=periodic_backup, daemon=True)
|
| 2124 |
backup_thread.start()
|
| 2125 |
+
logging.info("Periodic backup thread started.")
|
| 2126 |
+
else:
|
| 2127 |
+
logging.warning("Periodic backup will NOT run (HF_TOKEN for writing not set).")
|
| 2128 |
|
| 2129 |
+
if TELEGRAM_BOT_TOKEN:
|
| 2130 |
+
bot_thread = threading.Thread(target=start_bot_polling, daemon=True)
|
| 2131 |
+
bot_thread.start()
|
| 2132 |
+
logging.info("Telegram bot polling thread started.")
|
| 2133 |
+
else:
|
| 2134 |
+
logging.warning("TELEGRAM_BOT_TOKEN not set. Telegram bot features disabled.")
|
| 2135 |
|
| 2136 |
port = int(os.environ.get('PORT', 7860))
|
| 2137 |
+
logging.info(f"Starting Flask app on host 0.0.0.0 and port {port}")
|
| 2138 |
app.run(debug=False, host='0.0.0.0', port=port)
|