Update app.py
Browse files
app.py
CHANGED
|
@@ -19,7 +19,6 @@ app = Flask(__name__)
|
|
| 19 |
app.secret_key = 'dalarssi_secret_key_no_login'
|
| 20 |
DATA_FILE = 'data.json'
|
| 21 |
|
| 22 |
-
|
| 23 |
SYNC_FILES = [DATA_FILE]
|
| 24 |
|
| 25 |
REPO_ID = "Kgshop/dallarsi"
|
|
@@ -173,7 +172,6 @@ translations = {
|
|
| 173 |
}
|
| 174 |
}
|
| 175 |
|
| 176 |
-
|
| 177 |
def get_locale():
|
| 178 |
return session.get('lang', 'ru')
|
| 179 |
|
|
@@ -190,7 +188,6 @@ def set_language(language):
|
|
| 190 |
session['lang'] = language if language in translations else 'ru'
|
| 191 |
return redirect(request.referrer or url_for('catalog'))
|
| 192 |
|
| 193 |
-
|
| 194 |
def download_db_from_hf(specific_file=None, retries=DOWNLOAD_RETRIES, delay=DOWNLOAD_DELAY):
|
| 195 |
if not HF_TOKEN_READ and not HF_TOKEN_WRITE:
|
| 196 |
logging.warning("HF_TOKEN_READ/HF_TOKEN_WRITE not set. Download might fail for private repos.")
|
|
@@ -290,8 +287,6 @@ def periodic_backup():
|
|
| 290 |
upload_db_to_hf()
|
| 291 |
logging.info("Periodic backup finished.")
|
| 292 |
|
| 293 |
-
|
| 294 |
-
|
| 295 |
def load_data():
|
| 296 |
default_data = {'products': [], 'categories': [], 'orders': {}, 'employees': []}
|
| 297 |
try:
|
|
@@ -359,8 +354,6 @@ def save_data(data):
|
|
| 359 |
except Exception as e:
|
| 360 |
logging.error(f"Error saving data to {DATA_FILE}: {e}", exc_info=True)
|
| 361 |
|
| 362 |
-
|
| 363 |
-
|
| 364 |
CATALOG_TEMPLATE = '''
|
| 365 |
<!DOCTYPE html>
|
| 366 |
<html lang="{{ lang }}">
|
|
@@ -1285,7 +1278,6 @@ ADMIN_TEMPLATE = '''
|
|
| 1285 |
<a href="{{ url_for('catalog') }}" class="button" style="background-color: #6b8e23;"><i class="fas fa-store"></i> Перейти в каталог</a>
|
| 1286 |
</div>
|
| 1287 |
|
| 1288 |
-
|
| 1289 |
{% with messages = get_flashed_messages(with_categories=true) %}
|
| 1290 |
{% if messages %}
|
| 1291 |
{% for category, message in messages %}
|
|
@@ -1323,23 +1315,27 @@ ADMIN_TEMPLATE = '''
|
|
| 1323 |
</div>
|
| 1324 |
</details>
|
| 1325 |
|
| 1326 |
-
|
| 1327 |
-
|
| 1328 |
-
|
| 1329 |
-
|
| 1330 |
-
|
| 1331 |
-
|
| 1332 |
-
|
| 1333 |
-
|
| 1334 |
-
|
| 1335 |
-
|
| 1336 |
-
|
| 1337 |
-
|
| 1338 |
-
|
| 1339 |
-
|
| 1340 |
-
|
| 1341 |
-
|
| 1342 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1343 |
</div>
|
| 1344 |
</div>
|
| 1345 |
|
|
@@ -1358,23 +1354,27 @@ ADMIN_TEMPLATE = '''
|
|
| 1358 |
</div>
|
| 1359 |
</details>
|
| 1360 |
|
| 1361 |
-
|
| 1362 |
-
|
| 1363 |
-
|
| 1364 |
-
|
| 1365 |
-
|
| 1366 |
-
|
| 1367 |
-
|
| 1368 |
-
|
| 1369 |
-
|
| 1370 |
-
|
| 1371 |
-
|
| 1372 |
-
|
| 1373 |
-
|
| 1374 |
-
|
| 1375 |
-
|
| 1376 |
-
|
| 1377 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1378 |
</div>
|
| 1379 |
</div>
|
| 1380 |
</div>
|
|
@@ -1430,125 +1430,128 @@ ADMIN_TEMPLATE = '''
|
|
| 1430 |
</div>
|
| 1431 |
</details>
|
| 1432 |
|
| 1433 |
-
<
|
| 1434 |
-
|
| 1435 |
-
|
| 1436 |
-
|
| 1437 |
-
|
| 1438 |
-
|
| 1439 |
-
|
| 1440 |
-
|
| 1441 |
-
|
| 1442 |
-
|
| 1443 |
-
|
| 1444 |
-
|
| 1445 |
-
|
| 1446 |
-
|
| 1447 |
-
|
| 1448 |
-
|
| 1449 |
-
|
| 1450 |
-
|
| 1451 |
-
|
| 1452 |
-
|
| 1453 |
-
|
| 1454 |
-
|
| 1455 |
-
|
| 1456 |
-
|
| 1457 |
-
|
| 1458 |
-
|
| 1459 |
-
|
| 1460 |
-
|
| 1461 |
-
|
| 1462 |
-
|
| 1463 |
-
|
| 1464 |
-
|
| 1465 |
-
|
| 1466 |
-
|
| 1467 |
-
|
| 1468 |
-
|
| 1469 |
-
|
| 1470 |
-
|
| 1471 |
-
|
| 1472 |
-
<div class="item-actions">
|
| 1473 |
-
<button type="button" class="button" onclick="toggleEditForm('edit-form-{{ product.id }}')"><i class="fas fa-edit"></i> Редактировать</button>
|
| 1474 |
-
<form method="POST" style="margin:0;" onsubmit="return confirm('Вы уверены, что хотите удалить товар \'{{ product['name'] }}\'?');">
|
| 1475 |
-
<input type="hidden" name="action" value="delete_product">
|
| 1476 |
-
<input type="hidden" name="product_id" value="{{ product.id }}">
|
| 1477 |
-
<button type="submit" class="delete-button"><i class="fas fa-trash-alt"></i> Удалить</button>
|
| 1478 |
-
</form>
|
| 1479 |
-
</div>
|
| 1480 |
-
|
| 1481 |
-
<div id="edit-form-{{ product.id }}" class="edit-form-container">
|
| 1482 |
-
<h4><i class="fas fa-edit"></i> Редактирование: {{ product['name'] }}</h4>
|
| 1483 |
-
<form method="POST" enctype="multipart/form-data">
|
| 1484 |
-
<input type="hidden" name="action" value="edit_product">
|
| 1485 |
-
<input type="hidden" name="product_id" value="{{ product.id }}">
|
| 1486 |
-
<label>Название *:</label>
|
| 1487 |
-
<input type="text" name="name" value="{{ product['name'] }}" required>
|
| 1488 |
-
<label>Цена за линейку ({{ currency_code }}) *:</label>
|
| 1489 |
-
<input type="number" name="price" step="0.01" min="0" value="{{ product['price'] }}" required>
|
| 1490 |
-
<label>Штук в линейке *:</label>
|
| 1491 |
-
<input type="number" name="items_per_line" min="1" step="1" value="{{ product.get('items_per_line', 1) }}" required>
|
| 1492 |
-
<label>Описание:</label>
|
| 1493 |
-
<textarea name="description" rows="4">{{ product.get('description', '') }}</textarea>
|
| 1494 |
-
<label>Категория:</label>
|
| 1495 |
-
<select name="category">
|
| 1496 |
-
<option value="Без категории" {% if product.get('category', 'Без категории') == 'Без категории' %}selected{% endif %}>Без категории</option>
|
| 1497 |
-
{% for category in categories %}
|
| 1498 |
-
<option value="{{ category }}" {% if product.get('category') == category %}selected{% endif %}>{{ category }}</option>
|
| 1499 |
-
{% endfor %}
|
| 1500 |
-
</select>
|
| 1501 |
-
<label>Заменить фотографии (выберите новые файлы, до 10 шт.):</label>
|
| 1502 |
-
<input type="file" name="photos" accept="image/*" multiple>
|
| 1503 |
-
{% if product.get('photos') %}
|
| 1504 |
-
<p style="font-size: 0.85rem; margin-top: 5px;">Текущие фото:</p>
|
| 1505 |
-
<div class="photo-preview">
|
| 1506 |
-
{% for photo in product['photos'] %}
|
| 1507 |
-
<img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/{{ photo }}" alt="Фото {{ loop.index }}">
|
| 1508 |
-
{% endfor %}
|
| 1509 |
</div>
|
| 1510 |
-
{% endif %}
|
| 1511 |
-
<label>Цвета/Варианты:</label>
|
| 1512 |
-
<div id="edit-color-inputs-{{ product.id }}">
|
| 1513 |
-
{% set current_colors = product.get('colors', []) %}
|
| 1514 |
-
{% if current_colors and current_colors|select('ne', '')|list|length > 0 %}
|
| 1515 |
-
{% for color in current_colors %}
|
| 1516 |
-
{% if color.strip() %}
|
| 1517 |
-
<div class="color-input-group">
|
| 1518 |
-
<input type="text" name="colors" value="{{ color }}">
|
| 1519 |
-
<button type="button" class="remove-color-btn" onclick="removeColorInput(this)"><i class="fas fa-times"></i></button>
|
| 1520 |
-
</div>
|
| 1521 |
-
{% endif %}
|
| 1522 |
-
{% endfor %}
|
| 1523 |
-
{% else %}
|
| 1524 |
-
<div class="color-input-group">
|
| 1525 |
-
<input type="text" name="colors" placeholder="Например: Койот">
|
| 1526 |
-
<button type="button" class="remove-color-btn" onclick="removeColorInput(this)"><i class="fas fa-times"></i></button>
|
| 1527 |
-
</div>
|
| 1528 |
-
{% endif %}
|
| 1529 |
</div>
|
| 1530 |
-
|
| 1531 |
-
<
|
| 1532 |
-
|
| 1533 |
-
|
| 1534 |
-
|
|
|
|
|
|
|
|
|
|
| 1535 |
</div>
|
| 1536 |
-
|
| 1537 |
-
|
| 1538 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1539 |
</div>
|
| 1540 |
-
|
| 1541 |
-
|
| 1542 |
-
</form>
|
| 1543 |
</div>
|
|
|
|
|
|
|
|
|
|
| 1544 |
</div>
|
| 1545 |
-
|
| 1546 |
-
</div>
|
| 1547 |
-
{% else %}
|
| 1548 |
-
<p>Товаров пока нет.</p>
|
| 1549 |
-
{% endif %}
|
| 1550 |
</div>
|
| 1551 |
-
|
| 1552 |
</div>
|
| 1553 |
|
| 1554 |
<script>
|
|
@@ -1593,6 +1596,12 @@ ADMIN_TEMPLATE = '''
|
|
| 1593 |
function filterAdminProducts() {
|
| 1594 |
const searchTerm = document.getElementById('admin-product-search').value.toLowerCase().trim();
|
| 1595 |
const productItems = document.querySelectorAll('#admin-product-list .item');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1596 |
let visibleCount = 0;
|
| 1597 |
productItems.forEach(item => {
|
| 1598 |
const productName = item.getAttribute('data-product-name');
|
|
@@ -1609,8 +1618,6 @@ ADMIN_TEMPLATE = '''
|
|
| 1609 |
</html>
|
| 1610 |
'''
|
| 1611 |
|
| 1612 |
-
|
| 1613 |
-
|
| 1614 |
@app.route('/')
|
| 1615 |
def catalog():
|
| 1616 |
data = load_data()
|
|
@@ -1891,7 +1898,6 @@ def admin():
|
|
| 1891 |
elif not HF_TOKEN_WRITE and photos_files and any(f.filename for f in photos_files):
|
| 1892 |
flash("HF_TOKEN (write) не настроен. Фотографии не были загружены.", "warning")
|
| 1893 |
|
| 1894 |
-
|
| 1895 |
new_product = {
|
| 1896 |
'id': str(uuid.uuid4()),
|
| 1897 |
'name': name, 'price': price, 'description': description,
|
|
@@ -2037,7 +2043,6 @@ def admin():
|
|
| 2037 |
flash(f"Произошла внутренняя ошибка при выполнении действия '{action}'.", 'error')
|
| 2038 |
return redirect(url_for('admin'))
|
| 2039 |
|
| 2040 |
-
|
| 2041 |
current_data = load_data()
|
| 2042 |
display_products = sorted(current_data.get('products', []), key=lambda p: p.get('name', '').lower())
|
| 2043 |
display_categories = sorted(current_data.get('categories', []))
|
|
@@ -2077,8 +2082,6 @@ def force_download():
|
|
| 2077 |
flash(f"Ошибка при скачивании с Hugging Face: {e}", 'error')
|
| 2078 |
return redirect(url_for('admin'))
|
| 2079 |
|
| 2080 |
-
|
| 2081 |
-
|
| 2082 |
if __name__ == '__main__':
|
| 2083 |
logging.info("Application starting up. Performing initial data load/download...")
|
| 2084 |
download_db_from_hf()
|
|
|
|
| 19 |
app.secret_key = 'dalarssi_secret_key_no_login'
|
| 20 |
DATA_FILE = 'data.json'
|
| 21 |
|
|
|
|
| 22 |
SYNC_FILES = [DATA_FILE]
|
| 23 |
|
| 24 |
REPO_ID = "Kgshop/dallarsi"
|
|
|
|
| 172 |
}
|
| 173 |
}
|
| 174 |
|
|
|
|
| 175 |
def get_locale():
|
| 176 |
return session.get('lang', 'ru')
|
| 177 |
|
|
|
|
| 188 |
session['lang'] = language if language in translations else 'ru'
|
| 189 |
return redirect(request.referrer or url_for('catalog'))
|
| 190 |
|
|
|
|
| 191 |
def download_db_from_hf(specific_file=None, retries=DOWNLOAD_RETRIES, delay=DOWNLOAD_DELAY):
|
| 192 |
if not HF_TOKEN_READ and not HF_TOKEN_WRITE:
|
| 193 |
logging.warning("HF_TOKEN_READ/HF_TOKEN_WRITE not set. Download might fail for private repos.")
|
|
|
|
| 287 |
upload_db_to_hf()
|
| 288 |
logging.info("Periodic backup finished.")
|
| 289 |
|
|
|
|
|
|
|
| 290 |
def load_data():
|
| 291 |
default_data = {'products': [], 'categories': [], 'orders': {}, 'employees': []}
|
| 292 |
try:
|
|
|
|
| 354 |
except Exception as e:
|
| 355 |
logging.error(f"Error saving data to {DATA_FILE}: {e}", exc_info=True)
|
| 356 |
|
|
|
|
|
|
|
| 357 |
CATALOG_TEMPLATE = '''
|
| 358 |
<!DOCTYPE html>
|
| 359 |
<html lang="{{ lang }}">
|
|
|
|
| 1278 |
<a href="{{ url_for('catalog') }}" class="button" style="background-color: #6b8e23;"><i class="fas fa-store"></i> Перейти в каталог</a>
|
| 1279 |
</div>
|
| 1280 |
|
|
|
|
| 1281 |
{% with messages = get_flashed_messages(with_categories=true) %}
|
| 1282 |
{% if messages %}
|
| 1283 |
{% for category, message in messages %}
|
|
|
|
| 1315 |
</div>
|
| 1316 |
</details>
|
| 1317 |
|
| 1318 |
+
<details>
|
| 1319 |
+
<summary><i class="fas fa-list"></i> Список категорий</summary>
|
| 1320 |
+
<div class="form-content">
|
| 1321 |
+
{% if categories %}
|
| 1322 |
+
<div class="item-list">
|
| 1323 |
+
{% for category in categories %}
|
| 1324 |
+
<div class="item" style="display: flex; justify-content: space-between; align-items: center;">
|
| 1325 |
+
<span>{{ category }}</span>
|
| 1326 |
+
<form method="POST" style="margin: 0;" onsubmit="return confirm('Вы уверены, что хотите удалить категорию \'{{ category }}\'? Товары этой категории будут помечены как \'Без категории\'.');">
|
| 1327 |
+
<input type="hidden" name="action" value="delete_category">
|
| 1328 |
+
<input type="hidden" name="category_name" value="{{ category }}">
|
| 1329 |
+
<button type="submit" class="delete-button" style="padding: 5px 10px; font-size: 0.8rem; margin: 0;"><i class="fas fa-trash-alt"></i></button>
|
| 1330 |
+
</form>
|
| 1331 |
+
</div>
|
| 1332 |
+
{% endfor %}
|
| 1333 |
+
</div>
|
| 1334 |
+
{% else %}
|
| 1335 |
+
<p>Категорий пока нет.</p>
|
| 1336 |
+
{% endif %}
|
| 1337 |
+
</div>
|
| 1338 |
+
</details>
|
| 1339 |
</div>
|
| 1340 |
</div>
|
| 1341 |
|
|
|
|
| 1354 |
</div>
|
| 1355 |
</details>
|
| 1356 |
|
| 1357 |
+
<details>
|
| 1358 |
+
<summary><i class="fas fa-list"></i> Список сотрудников</summary>
|
| 1359 |
+
<div class="form-content">
|
| 1360 |
+
{% if employees %}
|
| 1361 |
+
<div class="item-list">
|
| 1362 |
+
{% for employee in employees %}
|
| 1363 |
+
<div class="item" style="display: flex; justify-content: space-between; align-items: center;">
|
| 1364 |
+
<span>{{ employee }}</span>
|
| 1365 |
+
<form method="POST" style="margin: 0;" onsubmit="return confirm('Вы уверены, что хотите удалить сотрудника \'{{ employee }}\'?');">
|
| 1366 |
+
<input type="hidden" name="action" value="delete_employee">
|
| 1367 |
+
<input type="hidden" name="employee_name" value="{{ employee }}">
|
| 1368 |
+
<button type="submit" class="delete-button" style="padding: 5px 10px; font-size: 0.8rem; margin: 0;"><i class="fas fa-trash-alt"></i></button>
|
| 1369 |
+
</form>
|
| 1370 |
+
</div>
|
| 1371 |
+
{% endfor %}
|
| 1372 |
+
</div>
|
| 1373 |
+
{% else %}
|
| 1374 |
+
<p>Сотрудников пока нет.</p>
|
| 1375 |
+
{% endif %}
|
| 1376 |
+
</div>
|
| 1377 |
+
</details>
|
| 1378 |
</div>
|
| 1379 |
</div>
|
| 1380 |
</div>
|
|
|
|
| 1430 |
</div>
|
| 1431 |
</details>
|
| 1432 |
|
| 1433 |
+
<details id="products-list-details">
|
| 1434 |
+
<summary><i class="fas fa-list"></i> Список товаров</summary>
|
| 1435 |
+
<div class="form-content">
|
| 1436 |
+
{% if products %}
|
| 1437 |
+
<div class="item-list" id="admin-product-list">
|
| 1438 |
+
{% for product in products %}
|
| 1439 |
+
<div class="item" data-product-name="{{ product.name|lower }}">
|
| 1440 |
+
<div style="display: flex; gap: 15px; align-items: flex-start;">
|
| 1441 |
+
<div class="photo-preview" style="flex-shrink: 0;">
|
| 1442 |
+
{% if product.get('photos') %}
|
| 1443 |
+
<a href="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/{{ product['photos'][0] }}" target="_blank" title="Посмотреть первое фото">
|
| 1444 |
+
<img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/{{ product['photos'][0] }}" alt="Фото">
|
| 1445 |
+
</a>
|
| 1446 |
+
{% else %}
|
| 1447 |
+
<img src="https://via.placeholder.com/70x70.png?text=N/A" alt="Нет фото">
|
| 1448 |
+
{% endif %}
|
| 1449 |
+
</div>
|
| 1450 |
+
<div style="flex-grow: 1;">
|
| 1451 |
+
<h3 style="margin-top: 0; margin-bottom: 5px; color: #3d3522;">
|
| 1452 |
+
{{ product['name'] }}
|
| 1453 |
+
{% if product.get('in_stock', True) %}
|
| 1454 |
+
<span class="status-indicator in-stock">В наличии</span>
|
| 1455 |
+
{% else %}
|
| 1456 |
+
<span class="status-indicator out-of-stock">Нет в наличии</span>
|
| 1457 |
+
{% endif %}
|
| 1458 |
+
{% if product.get('is_top', False) %}
|
| 1459 |
+
<span class="status-indicator top-product"><i class="fas fa-star"></i> Топ</span>
|
| 1460 |
+
{% endif %}
|
| 1461 |
+
</h3>
|
| 1462 |
+
<p><strong>Категория:</strong> {{ product.get('category', 'Без категории') }}</p>
|
| 1463 |
+
<p><strong>Цена за линейку:</strong> {{ "%.2f"|format(product['price']) }} {{ currency_code }}</p>
|
| 1464 |
+
<p><strong>Шт. в линейке:</strong> {{ product.get('items_per_line', 'N/A') }}</p>
|
| 1465 |
+
<p class="description" title="{{ product.get('description', '') }}"><strong>Описание:</strong> {{ product.get('description', 'N/A')[:150] }}{% if product.get('description', '')|length > 150 %}...{% endif %}</p>
|
| 1466 |
+
{% set colors = product.get('colors', []) %}
|
| 1467 |
+
<p><strong>Цвета/Вар-ты:</strong> {{ colors|select('ne', '')|join(', ') if colors|select('ne', '')|list|length > 0 else 'Нет' }}</p>
|
| 1468 |
+
{% if product.get('photos') and product['photos']|length > 1 %}
|
| 1469 |
+
<p style="font-size: 0.8rem; color: #a0aec0;">(Всего фото: {{ product['photos']|length }})</p>
|
| 1470 |
+
{% endif %}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1471 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1472 |
</div>
|
| 1473 |
+
|
| 1474 |
+
<div class="item-actions">
|
| 1475 |
+
<button type="button" class="button" onclick="toggleEditForm('edit-form-{{ product.id }}')"><i class="fas fa-edit"></i> Редактировать</button>
|
| 1476 |
+
<form method="POST" style="margin:0;" onsubmit="return confirm('Вы уверены, что хотите удалить товар \'{{ product['name'] }}\'?');">
|
| 1477 |
+
<input type="hidden" name="action" value="delete_product">
|
| 1478 |
+
<input type="hidden" name="product_id" value="{{ product.id }}">
|
| 1479 |
+
<button type="submit" class="delete-button"><i class="fas fa-trash-alt"></i> Удалить</button>
|
| 1480 |
+
</form>
|
| 1481 |
</div>
|
| 1482 |
+
|
| 1483 |
+
<div id="edit-form-{{ product.id }}" class="edit-form-container">
|
| 1484 |
+
<h4><i class="fas fa-edit"></i> Редактирование: {{ product['name'] }}</h4>
|
| 1485 |
+
<form method="POST" enctype="multipart/form-data">
|
| 1486 |
+
<input type="hidden" name="action" value="edit_product">
|
| 1487 |
+
<input type="hidden" name="product_id" value="{{ product.id }}">
|
| 1488 |
+
<label>Название *:</label>
|
| 1489 |
+
<input type="text" name="name" value="{{ product['name'] }}" required>
|
| 1490 |
+
<label>Цена за линейку ({{ currency_code }}) *:</label>
|
| 1491 |
+
<input type="number" name="price" step="0.01" min="0" value="{{ product['price'] }}" required>
|
| 1492 |
+
<label>Штук в линейке *:</label>
|
| 1493 |
+
<input type="number" name="items_per_line" min="1" step="1" value="{{ product.get('items_per_line', 1) }}" required>
|
| 1494 |
+
<label>Описание:</label>
|
| 1495 |
+
<textarea name="description" rows="4">{{ product.get('description', '') }}</textarea>
|
| 1496 |
+
<label>Категория:</label>
|
| 1497 |
+
<select name="category">
|
| 1498 |
+
<option value="Без категории" {% if product.get('category', 'Без категории') == 'Без категории' %}selected{% endif %}>Без категории</option>
|
| 1499 |
+
{% for category in categories %}
|
| 1500 |
+
<option value="{{ category }}" {% if product.get('category') == category %}selected{% endif %}>{{ category }}</option>
|
| 1501 |
+
{% endfor %}
|
| 1502 |
+
</select>
|
| 1503 |
+
<label>Заменить фотографии (выберите новые файлы, до 10 шт.):</label>
|
| 1504 |
+
<input type="file" name="photos" accept="image/*" multiple>
|
| 1505 |
+
{% if product.get('photos') %}
|
| 1506 |
+
<p style="font-size: 0.85rem; margin-top: 5px;">Текущие фото:</p>
|
| 1507 |
+
<div class="photo-preview">
|
| 1508 |
+
{% for photo in product['photos'] %}
|
| 1509 |
+
<img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/photos/{{ photo }}" alt="Фото {{ loop.index }}">
|
| 1510 |
+
{% endfor %}
|
| 1511 |
+
</div>
|
| 1512 |
+
{% endif %}
|
| 1513 |
+
<label>Цвета/Варианты:</label>
|
| 1514 |
+
<div id="edit-color-inputs-{{ product.id }}">
|
| 1515 |
+
{% set current_colors = product.get('colors', []) %}
|
| 1516 |
+
{% if current_colors and current_colors|select('ne', '')|list|length > 0 %}
|
| 1517 |
+
{% for color in current_colors %}
|
| 1518 |
+
{% if color.strip() %}
|
| 1519 |
+
<div class="color-input-group">
|
| 1520 |
+
<input type="text" name="colors" value="{{ color }}">
|
| 1521 |
+
<button type="button" class="remove-color-btn" onclick="removeColorInput(this)"><i class="fas fa-times"></i></button>
|
| 1522 |
+
</div>
|
| 1523 |
+
{% endif %}
|
| 1524 |
+
{% endfor %}
|
| 1525 |
+
{% else %}
|
| 1526 |
+
<div class="color-input-group">
|
| 1527 |
+
<input type="text" name="colors" placeholder="Например: Койот">
|
| 1528 |
+
<button type="button" class="remove-color-btn" onclick="removeColorInput(this)"><i class="fas fa-times"></i></button>
|
| 1529 |
+
</div>
|
| 1530 |
+
{% endif %}
|
| 1531 |
+
</div>
|
| 1532 |
+
<button type="button" class="button add-color-btn" style="margin-top: 5px;" onclick="addColorInput('edit-color-inputs-{{ product.id }}')"><i class="fas fa-palette"></i> Добавить поле для цвета</button>
|
| 1533 |
+
<br>
|
| 1534 |
+
<div style="margin-top: 15px;">
|
| 1535 |
+
<input type="checkbox" id="edit_in_stock_{{ product.id }}" name="in_stock" {% if product.get('in_stock', True) %}checked{% endif %}>
|
| 1536 |
+
<label for="edit_in_stock_{{ product.id }}" class="inline-label">В наличии</label>
|
| 1537 |
+
</div>
|
| 1538 |
+
<div style="margin-top: 5px;">
|
| 1539 |
+
<input type="checkbox" id="edit_is_top_{{ product.id }}" name="is_top" {% if product.get('is_top', False) %}checked{% endif %}>
|
| 1540 |
+
<label for="edit_is_top_{{ product.id }}" class="inline-label">Топ товар</label>
|
| 1541 |
+
</div>
|
| 1542 |
+
<br>
|
| 1543 |
+
<button type="submit" class="add-button" style="margin-top: 20px;"><i class="fas fa-save"></i> Сохранить изменения</button>
|
| 1544 |
+
</form>
|
| 1545 |
</div>
|
| 1546 |
+
</div>
|
| 1547 |
+
{% endfor %}
|
|
|
|
| 1548 |
</div>
|
| 1549 |
+
{% else %}
|
| 1550 |
+
<p>Товаров пока нет.</p>
|
| 1551 |
+
{% endif %}
|
| 1552 |
</div>
|
| 1553 |
+
</details>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1554 |
</div>
|
|
|
|
| 1555 |
</div>
|
| 1556 |
|
| 1557 |
<script>
|
|
|
|
| 1596 |
function filterAdminProducts() {
|
| 1597 |
const searchTerm = document.getElementById('admin-product-search').value.toLowerCase().trim();
|
| 1598 |
const productItems = document.querySelectorAll('#admin-product-list .item');
|
| 1599 |
+
const detailsTag = document.getElementById('products-list-details');
|
| 1600 |
+
|
| 1601 |
+
if (searchTerm.length > 0 && detailsTag && !detailsTag.hasAttribute('open')) {
|
| 1602 |
+
detailsTag.setAttribute('open', '');
|
| 1603 |
+
}
|
| 1604 |
+
|
| 1605 |
let visibleCount = 0;
|
| 1606 |
productItems.forEach(item => {
|
| 1607 |
const productName = item.getAttribute('data-product-name');
|
|
|
|
| 1618 |
</html>
|
| 1619 |
'''
|
| 1620 |
|
|
|
|
|
|
|
| 1621 |
@app.route('/')
|
| 1622 |
def catalog():
|
| 1623 |
data = load_data()
|
|
|
|
| 1898 |
elif not HF_TOKEN_WRITE and photos_files and any(f.filename for f in photos_files):
|
| 1899 |
flash("HF_TOKEN (write) не настроен. Фотографии не были загружены.", "warning")
|
| 1900 |
|
|
|
|
| 1901 |
new_product = {
|
| 1902 |
'id': str(uuid.uuid4()),
|
| 1903 |
'name': name, 'price': price, 'description': description,
|
|
|
|
| 2043 |
flash(f"Произошла внутренняя ошибка при выполнении действия '{action}'.", 'error')
|
| 2044 |
return redirect(url_for('admin'))
|
| 2045 |
|
|
|
|
| 2046 |
current_data = load_data()
|
| 2047 |
display_products = sorted(current_data.get('products', []), key=lambda p: p.get('name', '').lower())
|
| 2048 |
display_categories = sorted(current_data.get('categories', []))
|
|
|
|
| 2082 |
flash(f"Ошибка при скачивании с Hugging Face: {e}", 'error')
|
| 2083 |
return redirect(url_for('admin'))
|
| 2084 |
|
|
|
|
|
|
|
| 2085 |
if __name__ == '__main__':
|
| 2086 |
logging.info("Application starting up. Performing initial data load/download...")
|
| 2087 |
download_db_from_hf()
|