Улучшение генератора названий для букетов

- Добавлена функциональность для кнопок 'ВЗЯТЬ' и 'УДАЛИТЬ'
- Реализовано получение и удаление названий из базы данных
- Исправлена фильтрация названий
- Исправлена проблема с обработчиками событий
This commit is contained in:
2026-01-23 14:10:00 +03:00
parent 59f7a7c520
commit 84cfc5cd47
4 changed files with 277 additions and 135 deletions

View File

@@ -100,10 +100,10 @@
<br><span class="badge bg-secondary mt-1">В базе: <span id="bouquetNamesCount">{{ bouquet_names_count }}</span> названий</span> <br><span class="badge bg-secondary mt-1">В базе: <span id="bouquetNamesCount">{{ bouquet_names_count }}</span> названий</span>
</p> </p>
<div class="d-flex gap-2 mb-4"> <div class="d-flex gap-2 mb-4">
<button type="button" class="btn btn-outline-primary btn-sm"> <button type="button" class="btn btn-outline-primary btn-sm" id="populateNamesBtn">
<i class="bi bi-magic"></i> Пополнить базу названиями <i class="bi bi-magic"></i> Пополнить базу названиями
</button> </button>
<button type="button" class="btn btn-outline-secondary btn-sm"> <button type="button" class="btn btn-outline-secondary btn-sm" id="getThreeNamesBtn">
<i class="bi bi-refresh"></i> Дать три варианта <i class="bi bi-refresh"></i> Дать три варианта
</button> </button>
</div> </div>
@@ -111,30 +111,27 @@
<!-- Предложения названий --> <!-- Предложения названий -->
<div class="name-suggestions"> <div class="name-suggestions">
<!-- Строка 1 --> <!-- Строка 1 -->
<div class="d-flex justify-content-between align-items-center py-2 border-bottom"> <div class="d-flex justify-content-between align-items-center py-2 border-bottom name-row" data-name-id="">
<span class="text-muted small">Романтический букет роз</span> <span class="text-muted small name-text">-</span>
<div class="d-flex gap-1"> <div class="d-flex gap-1 name-buttons" style="display: none;">
<button type="button" class="btn btn-success btn-xs">Взять</button> <button type="button" class="btn btn-success btn-xs btn-take-name">Взять</button>
<button type="button" class="btn btn-outline-secondary btn-xs">Потом</button> <button type="button" class="btn btn-outline-danger btn-xs btn-delete-name">Удалить</button>
<button type="button" class="btn btn-outline-danger btn-xs">Убрать</button>
</div> </div>
</div> </div>
<!-- Строка 2 --> <!-- Строка 2 -->
<div class="d-flex justify-content-between align-items-center py-2 border-bottom"> <div class="d-flex justify-content-between align-items-center py-2 border-bottom name-row" data-name-id="">
<span class="text-muted small">Солнечный букет подсолнухов</span> <span class="text-muted small name-text">-</span>
<div class="d-flex gap-1"> <div class="d-flex gap-1 name-buttons" style="display: none;">
<button type="button" class="btn btn-success btn-xs">Взять</button> <button type="button" class="btn btn-success btn-xs btn-take-name">Взять</button>
<button type="button" class="btn btn-outline-secondary btn-xs">Потом</button> <button type="button" class="btn btn-outline-danger btn-xs btn-delete-name">Удалить</button>
<button type="button" class="btn btn-outline-danger btn-xs">Убрать</button>
</div> </div>
</div> </div>
<!-- Строка 3 --> <!-- Строка 3 -->
<div class="d-flex justify-content-between align-items-center py-2"> <div class="d-flex justify-content-between align-items-center py-2 name-row" data-name-id="">
<span class="text-muted small">Элегантный букет лотосов</span> <span class="text-muted small name-text">-</span>
<div class="d-flex gap-1"> <div class="d-flex gap-1 name-buttons" style="display: none;">
<button type="button" class="btn btn-success btn-xs">Взять</button> <button type="button" class="btn btn-success btn-xs btn-take-name">Взять</button>
<button type="button" class="btn btn-outline-secondary btn-xs">Потом</button> <button type="button" class="btn btn-outline-danger btn-xs btn-delete-name">Удалить</button>
<button type="button" class="btn btn-outline-danger btn-xs">Убрать</button>
</div> </div>
</div> </div>
</div> </div>
@@ -1412,57 +1409,14 @@ document.addEventListener('DOMContentLoaded', function() {
} }
// ========== ГЕНЕРАТОР НАЗВАНИЙ ========== // ========== ГЕНЕРАТОР НАЗВАНИЙ ==========
// Обработчик для кнопок "Взять"
document.querySelectorAll('.name-suggestions .btn-success').forEach(button => {
button.addEventListener('click', function() {
const suggestionText = this.closest('.d-flex').querySelector('.text-muted').textContent;
const nameInput = document.getElementById('id_name');
if (nameInput) {
nameInput.value = suggestionText;
// Улучшаем визуальный эффект
nameInput.style.borderColor = '#198754';
nameInput.style.boxShadow = '0 0 0 0.25rem rgba(25, 135, 84, 0.15)';
setTimeout(() => {
nameInput.style.borderColor = '';
nameInput.style.boxShadow = '';
}, 2000);
}
// Закрываем collapse
const collapse = document.getElementById('nameGeneratorCollapse');
const bsCollapse = new bootstrap.Collapse(collapse, { toggle: false });
bsCollapse.hide();
});
});
// Обработчик для кнопок "Убрать" // Обработчик для кнопки "Пополнить базу названиям<D18F><D0BC>"
document.querySelectorAll('.name-suggestions .btn-outline-danger').forEach(button => { const populateNamesBtn = document.getElementById('populateNamesBtn');
button.addEventListener('click', function() { if (populateNamesBtn) {
this.closest('.d-flex').remove(); populateNamesBtn.addEventListener('click', async function() {
}); const originalHTML = populateNamesBtn.innerHTML;
}); populateNamesBtn.innerHTML = '<span class="spinner-border spinner-border-sm"></span> Пополнение...';
populateNamesBtn.disabled = true;
// Обработчик для кнопок "Потом"
document.querySelectorAll('.name-suggestions .btn-outline-secondary').forEach(button => {
button.addEventListener('click', function() {
const row = this.closest('.d-flex');
row.style.opacity = '0.5';
row.style.textDecoration = 'line-through';
setTimeout(() => {
row.style.opacity = '1';
row.style.textDecoration = 'none';
}, 1000);
});
});
// ========== ГЕНЕРАТОР НАЗВАНИЙ - НОВЫЕ ОБРАБОТЧИКИ ==========
// Обработчик для кнопки "Сгенерировать" (LLM)
const generateBtn = document.querySelector('#nameGeneratorCollapse .btn-outline-primary');
if (generateBtn) {
generateBtn.addEventListener('click', async function() {
const originalHTML = generateBtn.innerHTML;
generateBtn.innerHTML = '<span class="spinner-border spinner-border-sm"></span> Генерация...';
generateBtn.disabled = true;
try { try {
const response = await fetch("{% url 'products:api-generate-bouquet-names' %}", { const response = await fetch("{% url 'products:api-generate-bouquet-names' %}", {
@@ -1478,89 +1432,94 @@ document.addEventListener('DOMContentLoaded', function() {
if (data.success) { if (data.success) {
// Обновляем счётчик // Обновляем счётчик
updateBouquetNamesCount(); updateBouquetNamesCount();
// Загружаем случайные 3
await loadRandomNames();
alert(data.message); alert(data.message);
} else { } else {
alert('Ошибка: ' + (data.error || 'Неизвестная ошибка')); alert('Ошибка: ' + (data.error || 'Неизвестная ошибка'));
} }
} catch (error) { } catch (error) {
console.error('Error:', error); console.error('Error:', error);
alert('Ошибка при генерации названий. Проверьте, что настроена AI-интеграция.'); alert('Ошибка при пополнении базы названий. Проверьте, что настроена AI-интеграция.');
} finally { } finally {
generateBtn.innerHTML = originalHTML; populateNamesBtn.innerHTML = originalHTML;
generateBtn.disabled = false; populateNamesBtn.disabled = false;
} }
}); });
} }
// Обработчик для кнопки "Случайное" // Обработчик для кнопки "Дать три варианта"
const randomBtn = document.querySelector('#nameGeneratorCollapse .btn-outline-secondary'); const getThreeNamesBtn = document.getElementById('getThreeNamesBtn');
if (randomBtn) { if (getThreeNamesBtn) {
randomBtn.addEventListener('click', loadRandomNames); getThreeNamesBtn.addEventListener('click', loadThreeRandomNames);
} }
async function loadRandomNames() { // Функция для загрузки трёх случайных названий
const originalHTML = randomBtn.innerHTML; async function loadThreeRandomNames() {
randomBtn.innerHTML = '<span class="spinner-border spinner-border-sm"></span> Загрузка...'; const originalHTML = getThreeNamesBtn.innerHTML;
randomBtn.disabled = true; getThreeNamesBtn.innerHTML = '<span class="spinner-border spinner-border-sm"></span> Загрузка...';
getThreeNamesBtn.disabled = true;
try { try {
const response = await fetch("{% url 'products:api-random-bouquet-names' %}?count=3"); const response = await fetch("{% url 'products:api-random-bouquet-names' %}?count=3");
const data = await response.json(); const data = await response.json();
if (data.names && data.names.length > 0) { if (data.names && data.names.length > 0) {
updateNameSuggestions(data.names); updateNameRows(data.names);
} else { } else {
alert('В базе пока нет названий. Сначала запустите загрузку из JSON или нажмите "Сгенерировать"'); alert('В базе пока нет названий. Сначала запустите загрузку из JSON или нажмите "Пополнить базу названиями"');
} }
} catch (error) { } catch (error) {
console.error('Error:', error); console.error('Error:', error);
alert('Ошибка при загрузке названий'); alert('Ошибка при загрузке названий');
} finally { } finally {
randomBtn.innerHTML = originalHTML; getThreeNamesBtn.innerHTML = originalHTML;
randomBtn.disabled = false; getThreeNamesBtn.disabled = false;
} }
} }
async function updateBouquetNamesCount() { // Функция для обновления строк с названиями
try { function updateNameRows(names) {
const response = await fetch("{% url 'products:api-random-bouquet-names' %}?count=999"); const rows = document.querySelectorAll('.name-row');
const data = await response.json();
const countEl = document.getElementById('bouquetNamesCount');
if (countEl) countEl.textContent = data.names.length;
} catch (error) {
console.error('Error updating count:', error);
}
}
function updateNameSuggestions(names) { rows.forEach((row, index) => {
const container = document.querySelector('.name-suggestions'); const nameTextElement = row.querySelector('.name-text');
container.innerHTML = ''; const buttonsElement = row.querySelector('.name-buttons');
names.forEach((name, index) => { if (index < names.length) {
const row = document.createElement('div'); // Если есть название для этой строки
row.className = 'd-flex justify-content-between align-items-center py-2' + const nameObj = names[index];
(index < names.length - 1 ? ' border-bottom' : ''); nameTextElement.textContent = nameObj.name;
row.innerHTML = ` row.setAttribute('data-name-id', nameObj.id);
<span class="text-muted small">${name}</span>
<div class="d-flex gap-1"> // Показываем кнопки
<button type="button" class="btn btn-success btn-xs btn-apply-name">Взять</button> buttonsElement.style.display = 'flex';
<button type="button" class="btn btn-outline-danger btn-xs btn-remove-name">Убрать</button> } else {
</div> // Если нет названия для этой строки
`; nameTextElement.textContent = '-';
container.appendChild(row); row.setAttribute('data-name-id', '');
// Скрываем кнопки
buttonsElement.style.display = 'none';
}
}); });
attachNameButtonHandlers();
// Устанавливаем обработчики событий для новых кнопок
attachNameRowHandlers();
} }
function attachNameButtonHandlers() { // Обработчики для кнопок "Взять" и "Удалить"
document.querySelectorAll('.btn-apply-name').forEach(button => { function attachNameRowHandlers() {
button.addEventListener('click', function() { // Обработчик для кнопки "Взять"
const name = this.closest('.d-flex').querySelector('.text-muted').textContent; document.querySelectorAll('.btn-take-name').forEach(button => {
// Проверяем, был ли уже добавлен обработчик
if (!button.dataset.handlerAttached) {
button.addEventListener('click', async function() {
const row = this.closest('.name-row');
const nameText = row.querySelector('.name-text').textContent;
const nameId = row.getAttribute('data-name-id');
const nameInput = document.getElementById('id_name'); const nameInput = document.getElementById('id_name');
if (nameInput) { if (nameInput) {
nameInput.value = name; nameInput.value = nameText;
nameInput.style.borderColor = '#198754'; nameInput.style.borderColor = '#198754';
nameInput.style.boxShadow = '0 0 0 0.25rem rgba(25, 135, 84, 0.15)'; nameInput.style.boxShadow = '0 0 0 0.25rem rgba(25, 135, 84, 0.15)';
setTimeout(() => { setTimeout(() => {
@@ -1568,16 +1527,117 @@ document.addEventListener('DOMContentLoaded', function() {
nameInput.style.boxShadow = ''; nameInput.style.boxShadow = '';
}, 2000); }, 2000);
} }
});
// Удаляем название из базы данных
if (nameId) {
await removeNameFromDatabase(nameId);
// Заменяем название новым из базы данных
await replaceNameInRow(row);
}
}); });
document.querySelectorAll('.btn-remove-name').forEach(button => { // Отмечаем, что обработчик уже добавлен
button.addEventListener('click', function() { button.dataset.handlerAttached = 'true';
this.closest('.d-flex').remove(); }
}); });
// Обработчик для кнопки "Удалить"
document.querySelectorAll('.btn-delete-name').forEach(button => {
// Проверяем, был ли уже добавлен обработчик
if (!button.dataset.handlerAttached) {
button.addEventListener('click', async function() {
const row = this.closest('.name-row');
const nameId = row.getAttribute('data-name-id');
// Удаляем название из базы данных
if (nameId) {
await removeNameFromDatabase(nameId);
// Заменяем название новым из базы данных
await replaceNameInRow(row);
}
});
// Отмечаем, что обработчик уже добавлен
button.dataset.handlerAttached = 'true';
}
}); });
} }
// Функция для удаления названия из базы данных
async function removeNameFromDatabase(nameId) {
try {
const response = await fetch(`{% url 'products:api-delete-bouquet-name' 0 %}`.replace('/0/', `/${nameId}/`), {
method: 'DELETE',
headers: {
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value,
'Content-Type': 'application/json'
}
});
if (response.ok) {
// Обновляем счётчик
updateBouquetNamesCount();
} else {
console.error('Failed to delete name from database');
}
} catch (error) {
console.error('Error deleting name:', error);
}
}
// Функция для замены названия в строке новым из базы данных
async function replaceNameInRow(row) {
try {
const response = await fetch("{% url 'products:api-random-bouquet-names' %}?count=1");
const data = await response.json();
if (data.names && data.names.length > 0) {
const newName = data.names[0];
const nameTextElement = row.querySelector('.name-text');
const buttonsElement = row.querySelector('.name-buttons');
nameTextElement.textContent = newName.name;
row.setAttribute('data-name-id', newName.id);
// Показываем кнопки
buttonsElement.style.display = 'flex';
} else {
// Если в базе больше нет названий
const nameTextElement = row.querySelector('.name-text');
const buttonsElement = row.querySelector('.name-buttons');
nameTextElement.textContent = '-';
row.setAttribute('data-name-id', '');
// Скрываем кнопки
buttonsElement.style.display = 'none';
}
} catch (error) {
console.error('Error replacing name:', error);
}
}
// Функция для обновления счётчика названий в базе
async function updateBouquetNamesCount() {
try {
const response = await fetch("{% url 'products:api-get-bouquet-names-count' %}");
const data = await response.json();
const countEl = document.getElementById('bouquetNamesCount');
if (countEl) countEl.textContent = data.count;
} catch (error) {
console.error('Error updating count:', error);
}
}
// Инициализация обработчиков кнопок
document.addEventListener('click', function(e) {
if (e.target.classList.contains('btn-take-name') || e.target.classList.contains('btn-delete-name')) {
attachNameRowHandlers();
}
});
// ========== ВАЛИДАЦИЯ ПЕРЕД ОТПРАВКОЙ ========== // ========== ВАЛИДАЦИЯ ПЕРЕД ОТПРАВКОЙ ==========
const kitForm = document.querySelector('form[method="post"]'); const kitForm = document.querySelector('form[method="post"]');
if (kitForm) { if (kitForm) {

View File

@@ -58,6 +58,8 @@ urlpatterns = [
path('api/bulk-update-categories/', api_views.bulk_update_categories, name='api-bulk-update-categories'), path('api/bulk-update-categories/', api_views.bulk_update_categories, name='api-bulk-update-categories'),
path('api/bouquet-names/random/', api_views.RandomBouquetNamesView.as_view(), name='api-random-bouquet-names'), path('api/bouquet-names/random/', api_views.RandomBouquetNamesView.as_view(), name='api-random-bouquet-names'),
path('api/bouquet-names/generate/', api_views.GenerateBouquetNamesView.as_view(), name='api-generate-bouquet-names'), path('api/bouquet-names/generate/', api_views.GenerateBouquetNamesView.as_view(), name='api-generate-bouquet-names'),
path('api/bouquet-names/<int:pk>/delete/', api_views.DeleteBouquetNameView.as_view(), name='api-delete-bouquet-name'),
path('api/bouquet-names/count/', api_views.GetBouquetNamesCountView.as_view(), name='api-get-bouquet-names-count'),
# Photo processing status API (for AJAX polling) # Photo processing status API (for AJAX polling)
path('api/photos/status/<str:task_id>/', photo_status_api.photo_processing_status, name='api-photo-status'), path('api/photos/status/<str:task_id>/', photo_status_api.photo_processing_status, name='api-photo-status'),

View File

@@ -1816,8 +1816,12 @@ class RandomBouquetNamesView(View):
count = int(request.GET.get('count', 3)) count = int(request.GET.get('count', 3))
# Ограничиваем максимум до 100 # Ограничиваем максимум до 100
count = min(count, 100) count = min(count, 100)
names = list(BouquetName.objects.order_by('?')[:count].values_list('name', flat=True))
return JsonResponse({'names': names}) # Получаем случайные названия с ID (любые, не только одобренные)
queryset = BouquetName.objects.order_by('?')[:count]
names_data = [{'id': obj.id, 'name': obj.name} for obj in queryset]
return JsonResponse({'names': names_data})
class GenerateBouquetNamesView(View): class GenerateBouquetNamesView(View):
@@ -1843,3 +1847,25 @@ class GenerateBouquetNamesView(View):
}) })
else: else:
return JsonResponse({'success': False, 'error': msg}, status=400) return JsonResponse({'success': False, 'error': msg}, status=400)
class DeleteBouquetNameView(View):
"""Удаляет конкретное название из базы"""
def delete(self, request, pk):
try:
name_obj = BouquetName.objects.get(pk=pk)
name_obj.delete()
return JsonResponse({'success': True})
except BouquetName.DoesNotExist:
return JsonResponse({'success': False, 'error': 'Название не найдено'}, status=404)
except Exception as e:
return JsonResponse({'success': False, 'error': str(e)}, status=500)
class GetBouquetNamesCountView(View):
"""Возвращает количество названий в базе"""
def get(self, request):
count = BouquetName.objects.count()
return JsonResponse({'count': count})

54
test_bouquet_api.py Normal file
View File

@@ -0,0 +1,54 @@
"""
Простой тест для проверки API-эндпоинтов генератора названий букетов
"""
import os
import sys
import django
from django.test import Client
# Настройка Django
sys.path.append(r'c:\Users\team_\Desktop\test_qwen')
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myproject.settings')
django.setup()
def test_bouquet_api_endpoints():
client = Client()
print("Тестируем API-эндпоинты для названий букетов...")
# Тестируем получение случайных названий
print("\n1. Тестируем получение случайных названий...")
response = client.get('/products/api/bouquet-names/random/?count=3')
print(f"Статус: {response.status_code}")
if response.status_code == 200:
data = response.json()
print(f"Получено названий: {len(data.get('names', []))}")
print(f"Примеры: {data.get('names', [])[:2]}")
else:
print(f"Ошибка: {response.content.decode()}")
# Тестируем получение количества названий
print("\n2. Тестируем получение количества названий...")
response = client.get('/products/api/bouquet-names/count/')
print(f"Статус: {response.status_code}")
if response.status_code == 200:
data = response.json()
print(f"Количество названий в базе: {data.get('count', 0)}")
else:
print(f"Ошибка: {response.content.decode()}")
# Попробуем сгенерировать названия (только если есть настройки для AI)
print("\n3. Попробуем сгенерировать названия...")
try:
response = client.post('/products/api/bouquet-names/generate/', {'count': 5})
print(f"Статус: {response.status_code}")
if response.status_code == 200:
data = response.json()
print(f"Результат генерации: {data}")
else:
print(f"Ошибка генерации: {response.content.decode()}")
except Exception as e:
print(f"Исключение при генерации: {e}")
if __name__ == "__main__":
test_bouquet_api_endpoints()