diff --git a/myproject/inventory/models.py b/myproject/inventory/models.py index 8685d8e..b320408 100644 --- a/myproject/inventory/models.py +++ b/myproject/inventory/models.py @@ -667,7 +667,16 @@ class ShowcaseItem(models.Model): from datetime import timedelta self.status = 'in_cart' - self.locked_by_user = user + + # Проверяем тип пользователя - locked_by_user только для CustomUser + from accounts.models import CustomUser + if isinstance(user, CustomUser): + self.locked_by_user = user + else: + # Для PlatformAdmin и других типов пользователей поле оставляем пустым + # Блокировка будет работать через cart_session_id + self.locked_by_user = None + self.cart_lock_expires_at = timezone.now() + timedelta(minutes=duration_minutes) self.cart_session_id = session_id self.save(update_fields=['status', 'locked_by_user', 'cart_lock_expires_at', 'cart_session_id', 'updated_at']) diff --git a/myproject/pos/views.py b/myproject/pos/views.py index 3402d4c..85d0584 100644 --- a/myproject/pos/views.py +++ b/myproject/pos/views.py @@ -263,13 +263,25 @@ def pos_terminal(request): if showcase_item_ids: # Проверяем, что все указанные ShowcaseItem заблокированы на текущего пользователя - locked_items = ShowcaseItem.objects.filter( - id__in=showcase_item_ids, - product_kit=kit, - status='in_cart', - locked_by_user=request.user, - cart_lock_expires_at__gt=timezone.now() - ) + from accounts.models import CustomUser + if isinstance(request.user, CustomUser): + locked_items = ShowcaseItem.objects.filter( + id__in=showcase_item_ids, + product_kit=kit, + status='in_cart', + locked_by_user=request.user, + cart_lock_expires_at__gt=timezone.now() + ) + else: + # Для PlatformAdmin используем проверку по сессии + session_id = request.session.session_key or '' + locked_items = ShowcaseItem.objects.filter( + id__in=showcase_item_ids, + product_kit=kit, + status='in_cart', + cart_session_id=session_id, + cart_lock_expires_at__gt=timezone.now() + ) locked_count = locked_items.count() @@ -655,11 +667,22 @@ def remove_showcase_kit_from_cart(request, kit_id): showcase_item_ids = [] # Базовый фильтр - экземпляры этого комплекта, заблокированные текущим пользователем - qs = ShowcaseItem.objects.filter( - product_kit=kit, - status='in_cart', - locked_by_user=request.user - ) + from accounts.models import CustomUser + + if isinstance(request.user, CustomUser): + qs = ShowcaseItem.objects.filter( + product_kit=kit, + status='in_cart', + locked_by_user=request.user + ) + else: + # Для PlatformAdmin используем проверку по сессии + session_id = request.session.session_key or '' + qs = ShowcaseItem.objects.filter( + product_kit=kit, + status='in_cart', + cart_session_id=session_id + ) # Если указаны конкретные ID - фильтруем только их if showcase_item_ids: @@ -710,10 +733,22 @@ def release_all_my_showcase_locks(request): try: # Снимаем ВСЕ блокировки текущего пользователя - updated_count = ShowcaseItem.objects.filter( - status='in_cart', - locked_by_user=request.user - ).update( + from accounts.models import CustomUser + + if isinstance(request.user, CustomUser): + qs_to_release = ShowcaseItem.objects.filter( + status='in_cart', + locked_by_user=request.user + ) + else: + # Для PlatformAdmin фильтруем по сессии + session_id = request.session.session_key or '' + qs_to_release = ShowcaseItem.objects.filter( + status='in_cart', + cart_session_id=session_id + ) + + updated_count = qs_to_release.update( status='available', locked_by_user=None, cart_lock_expires_at=None, diff --git a/myproject/products/admin.py b/myproject/products/admin.py index c7cc0f4..29e8372 100644 --- a/myproject/products/admin.py +++ b/myproject/products/admin.py @@ -13,6 +13,7 @@ from .models import ProductPhoto, ProductKitPhoto, ProductCategoryPhoto from .models import ProductVariantGroup, KitItemPriority, SKUCounter, CostPriceHistory from .models import ConfigurableProduct, ConfigurableProductOption, ConfigurableProductAttribute from .models import UnitOfMeasure, ProductSalesUnit +from .models import BouquetName from .admin_displays import ( format_quality_badge, format_quality_display, @@ -1086,3 +1087,42 @@ class ConfigurableProductAdmin(TenantAdminOnlyMixin, admin.ModelAdmin): count ) get_options_count.short_description = 'Вариантов' + + +@admin.register(BouquetName) +class BouquetNameAdmin(TenantAdminOnlyMixin, admin.ModelAdmin): + """ + Административный интерфейс для управления названиями букетов + """ + list_display = ('name', 'language', 'is_approved', 'usage_count', 'generated_at') + list_filter = ('language', 'is_approved') + search_fields = ('name',) + filter_horizontal = ('color_tags', 'occasion_tags', 'style_tags') + actions = ['approve_selected', 'reject_selected'] + + fieldsets = ( + ('Основная информация', { + 'fields': ('name', 'language', 'is_approved') + }), + ('Теги', { + 'fields': ('color_tags', 'occasion_tags', 'style_tags') + }), + ('Статистика', { + 'fields': ('usage_count', 'generated_at', 'approved_at') + }), + ) + + readonly_fields = ('usage_count', 'generated_at', 'approved_at') + + def approve_selected(self, request, queryset): + from django.db import models + queryset.update(is_approved=True, approved_at=models.DateTimeField(auto_now=True)) + self.message_user(request, "Выбранные названия были одобрены") + + approve_selected.short_description = "Одобрить выбранные названия" + + def reject_selected(self, request, queryset): + queryset.update(is_approved=False, approved_at=None) + self.message_user(request, "Выбранные названия были отклонены") + + reject_selected.short_description = "Отклонить выбранные названия" diff --git a/myproject/products/migrations/0002_bouquetname.py b/myproject/products/migrations/0002_bouquetname.py new file mode 100644 index 0000000..9a8d1a2 --- /dev/null +++ b/myproject/products/migrations/0002_bouquetname.py @@ -0,0 +1,33 @@ +# Generated by Django 5.0.10 on 2026-01-22 10:09 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('products', '0001_add_sales_unit_to_kititem'), + ] + + operations = [ + migrations.CreateModel( + name='BouquetName', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=100, unique=True, verbose_name='Название букета')), + ('language', models.CharField(default='russian', max_length=10, verbose_name='Язык')), + ('is_approved', models.BooleanField(default=False, verbose_name='Одобрено для использования')), + ('usage_count', models.PositiveIntegerField(default=0, verbose_name='Количество использований')), + ('generated_at', models.DateTimeField(auto_now_add=True, verbose_name='Дата генерации')), + ('approved_at', models.DateTimeField(blank=True, null=True, verbose_name='Дата одобрения')), + ('color_tags', models.ManyToManyField(blank=True, related_name='bouquet_names_by_color', to='products.producttag', verbose_name='Цветные теги')), + ('occasion_tags', models.ManyToManyField(blank=True, related_name='bouquet_names_by_occasion', to='products.producttag', verbose_name='Теги по поводу')), + ('style_tags', models.ManyToManyField(blank=True, related_name='bouquet_names_by_style', to='products.producttag', verbose_name='Теги по стилю')), + ], + options={ + 'verbose_name': 'Название букета', + 'verbose_name_plural': 'Названия букетов', + 'indexes': [models.Index(fields=['language', 'is_approved'], name='products_bo_languag_8622de_idx'), models.Index(fields=['usage_count'], name='products_bo_usage_c_4ce5b8_idx')], + }, + ), + ] diff --git a/myproject/products/models/__init__.py b/myproject/products/models/__init__.py index 6fd455a..db93862 100644 --- a/myproject/products/models/__init__.py +++ b/myproject/products/models/__init__.py @@ -49,6 +49,9 @@ from .photos import BasePhoto, ProductPhoto, ProductKitPhoto, ProductCategoryPho # Задачи импорта from .import_job import ProductImportJob +# Названия букетов +from .bouquet_names import BouquetName + # Явно указываем, что экспортируется при импорте * __all__ = [ # Managers @@ -98,4 +101,7 @@ __all__ = [ # Import Jobs 'ProductImportJob', + + # Bouquet Names + 'BouquetName', ] diff --git a/myproject/products/models/bouquet_names.py b/myproject/products/models/bouquet_names.py new file mode 100644 index 0000000..7e0fb06 --- /dev/null +++ b/myproject/products/models/bouquet_names.py @@ -0,0 +1,73 @@ +from django.db import models +from .categories import ProductTag + + +class BouquetName(models.Model): + """ + Модель для хранения предопределенных названий букетов с метаинформацией + """ + name = models.CharField( + max_length=100, + unique=True, + verbose_name="Название букета" + ) + + # Категории характеристик + color_tags = models.ManyToManyField( + ProductTag, + blank=True, + related_name='bouquet_names_by_color', + verbose_name="Цветные теги" + ) + + occasion_tags = models.ManyToManyField( + ProductTag, + blank=True, + related_name='bouquet_names_by_occasion', + verbose_name="Теги по поводу" + ) + + style_tags = models.ManyToManyField( + ProductTag, + blank=True, + related_name='bouquet_names_by_style', + verbose_name="Теги по стилю" + ) + + language = models.CharField( + max_length=10, + default='russian', + verbose_name="Язык" + ) + + is_approved = models.BooleanField( + default=False, + verbose_name="Одобрено для использования" + ) + + usage_count = models.PositiveIntegerField( + default=0, + verbose_name="Количество использований" + ) + + generated_at = models.DateTimeField( + auto_now_add=True, + verbose_name="Дата генерации" + ) + + approved_at = models.DateTimeField( + null=True, + blank=True, + verbose_name="Дата одобрения" + ) + + class Meta: + verbose_name = "Название букета" + verbose_name_plural = "Названия букетов" + indexes = [ + models.Index(fields=['language', 'is_approved']), + models.Index(fields=['usage_count']), + ] + + def __str__(self): + return self.name diff --git a/myproject/products/services/ai/bouquet_names.py b/myproject/products/services/ai/bouquet_names.py index 44a7757..2857380 100644 --- a/myproject/products/services/ai/bouquet_names.py +++ b/myproject/products/services/ai/bouquet_names.py @@ -2,12 +2,14 @@ from typing import Tuple, Optional, Dict, List from .base import BaseAIProductService import logging +from django.db import models + logger = logging.getLogger(__name__) class BouquetNameGenerator(BaseAIProductService): """ - Сервис для генерации названий букетов с помощью AI + Сервис для генерации и управления названиями букетов """ DEFAULT_SYSTEM_PROMPT = ( @@ -128,3 +130,79 @@ class BouquetNameGenerator(BaseAIProductService): unique_names.append(name) return unique_names + + def generate_and_store( + self, + count: int = 500, + characteristics: Optional[str] = None, + occasion: Optional[str] = None, + language: str = "russian" + ) -> Tuple[bool, str, Optional[Dict]]: + """ + Генерирует названия и сохраняет в базу данных + """ + from products.models import BouquetName + + success, msg, data = self.generate(count, characteristics, occasion, language) + + if success and data: + # Сохраняем названия в базу + stored_count = 0 + for name in data['names']: + BouquetName.objects.get_or_create( + name=name, + language=language, + defaults={ + 'is_approved': False + } + ) + stored_count += 1 + + return True, f"Сгенерировано и сохранено {stored_count} названий для букетов", data + + return success, msg, data + + def get_approved_names( + self, + color_tags: Optional[List[str]] = None, + occasion_tags: Optional[List[str]] = None, + style_tags: Optional[List[str]] = None, + language: str = "russian", + limit: int = 100 + ) -> List[str]: + """ + Получает одобренные названия с фильтрацией по тегам + """ + from products.models import BouquetName + + queryset = BouquetName.objects.filter( + is_approved=True, + language=language + ) + + if color_tags: + queryset = queryset.filter(color_tags__name__in=color_tags) + + if occasion_tags: + queryset = queryset.filter(occasion_tags__name__in=occasion_tags) + + if style_tags: + queryset = queryset.filter(style_tags__name__in=style_tags) + + # Сортируем по популярности + queryset = queryset.order_by('-usage_count') + + return list(queryset.values_list('name', flat=True)[:limit]) + + def mark_as_used(self, name: str, language: str = "russian") -> None: + """ + Увеличивает счетчик использования названия + """ + from products.models import BouquetName + + BouquetName.objects.filter( + name=name, + language=language + ).update( + usage_count=models.F('usage_count') + 1 + ) diff --git a/myproject/products/templates/products/productkit_create.html b/myproject/products/templates/products/productkit_create.html index cb69366..53df92b 100644 --- a/myproject/products/templates/products/productkit_create.html +++ b/myproject/products/templates/products/productkit_create.html @@ -86,6 +86,62 @@
+ +
+ +
+
+

+ Сгенерируйте привлекательное название для вашего букета автоматически +
В базе: {{ bouquet_names_count }} названий +

+
+ + +
+ + +
+ +
+ Романтический букет роз +
+ + + +
+
+ +
+ Солнечный букет подсолнухов +
+ + + +
+
+ +
+ Элегантный букет лотосов +
+ + + +
+
+
+
+
+
+
@@ -410,6 +466,50 @@ color: #0d6efd; } +/* Стили для генератора названий */ +.cursor-pointer { + cursor: pointer; +} + +.card-header[data-bs-toggle="collapse"]:hover { + background-color: #f8f9fa; +} + +.card-header[data-bs-toggle="collapse"] .bi-chevron-down { + transition: transform 0.2s ease; +} + +.collapse.show .card-header[data-bs-toggle="collapse"] .bi-chevron-down { + transform: rotate(180deg); +} + +/* Кнопки очень маленького размера */ +.btn-xs { + padding: 0.125rem 0.25rem; + font-size: 0.75rem; + line-height: 1; + border-radius: 0.2rem; +} + +.btn-xs:hover { + transform: translateY(-1px); +} + +/* Стили для списка предложений */ +.name-suggestions { + background-color: #f8f9fa; + border-radius: 0.375rem; + overflow: hidden; +} + +.name-suggestions .text-muted { + font-size: 0.875rem; +} + +.name-suggestions .border-bottom { + border-color: #e9ecef !important; +} + /* Стили для полей корректировки цены */ #id_increase_percent:disabled, #id_increase_amount:disabled, @@ -1311,6 +1411,173 @@ 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(); + }); + }); + + // Обработчик для кнопок "Убрать" + document.querySelectorAll('.name-suggestions .btn-outline-danger').forEach(button => { + button.addEventListener('click', function() { + this.closest('.d-flex').remove(); + }); + }); + + // Обработчик для кнопок "Потом" + 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 = ' Генерация...'; + generateBtn.disabled = true; + + try { + const response = await fetch("{% url 'products:api-generate-bouquet-names' %}", { + method: 'POST', + headers: { + 'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value, + 'Content-Type': 'application/x-www-form-urlencoded' + }, + body: new URLSearchParams({'count': 100}) + }); + const data = await response.json(); + + if (data.success) { + // Обновляем счётчик + updateBouquetNamesCount(); + // Загружаем случайные 3 + await loadRandomNames(); + alert(data.message); + } else { + alert('Ошибка: ' + (data.error || 'Неизвестная ошибка')); + } + } catch (error) { + console.error('Error:', error); + alert('Ошибка при генерации названий. Проверьте, что настроена AI-интеграция.'); + } finally { + generateBtn.innerHTML = originalHTML; + generateBtn.disabled = false; + } + }); + } + + // Обработчик для кнопки "Случайное" + const randomBtn = document.querySelector('#nameGeneratorCollapse .btn-outline-secondary'); + if (randomBtn) { + randomBtn.addEventListener('click', loadRandomNames); + } + + async function loadRandomNames() { + const originalHTML = randomBtn.innerHTML; + randomBtn.innerHTML = ' Загрузка...'; + randomBtn.disabled = true; + + try { + const response = await fetch("{% url 'products:api-random-bouquet-names' %}?count=3"); + const data = await response.json(); + + if (data.names && data.names.length > 0) { + updateNameSuggestions(data.names); + } else { + alert('В базе пока нет названий. Сначала запустите загрузку из JSON или нажмите "Сгенерировать"'); + } + } catch (error) { + console.error('Error:', error); + alert('Ошибка при загрузке названий'); + } finally { + randomBtn.innerHTML = originalHTML; + randomBtn.disabled = false; + } + } + + async function updateBouquetNamesCount() { + try { + const response = await fetch("{% url 'products:api-random-bouquet-names' %}?count=999"); + 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) { + const container = document.querySelector('.name-suggestions'); + container.innerHTML = ''; + + names.forEach((name, index) => { + const row = document.createElement('div'); + row.className = 'd-flex justify-content-between align-items-center py-2' + + (index < names.length - 1 ? ' border-bottom' : ''); + row.innerHTML = ` + ${name} +
+ + +
+ `; + container.appendChild(row); + }); + attachNameButtonHandlers(); + } + + function attachNameButtonHandlers() { + document.querySelectorAll('.btn-apply-name').forEach(button => { + button.addEventListener('click', function() { + const name = this.closest('.d-flex').querySelector('.text-muted').textContent; + const nameInput = document.getElementById('id_name'); + if (nameInput) { + nameInput.value = name; + 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); + } + }); + }); + + document.querySelectorAll('.btn-remove-name').forEach(button => { + button.addEventListener('click', function() { + this.closest('.d-flex').remove(); + }); + }); + } + // ========== ВАЛИДАЦИЯ ПЕРЕД ОТПРАВКОЙ ========== const kitForm = document.querySelector('form[method="post"]'); if (kitForm) { diff --git a/myproject/products/tests/test_ai_bouquet_names.py b/myproject/products/tests/test_ai_bouquet_names.py index 918086e..fe16050 100644 --- a/myproject/products/tests/test_ai_bouquet_names.py +++ b/myproject/products/tests/test_ai_bouquet_names.py @@ -1,5 +1,6 @@ from django_tenants.test.cases import TenantTestCase from products.services import BouquetNameGenerator +from products.models import BouquetName, ProductTag from unittest.mock import patch, MagicMock @@ -167,3 +168,123 @@ I'm sorry, but I can't help with that right now. """ names = self.generator._parse_response(response_text) self.assertEqual(len(names), 0) + + @patch('products.services.ai.bouquet_names.BouquetNameGenerator.get_glm_service') + def test_generate_and_store(self, mock_get_glm_service): + """ + Тест генерации и сохранения названий в базе данных + """ + # Создаем мок-объект сервиса + mock_service = MagicMock() + mock_service.generate_text.return_value = ( + True, + "Текст успешно сгенерирован", + { + 'generated_text': ( + "1. Розавая мечта\n" + "2. Лиловые настроения\n" + "3. Яркий букет для дня рождения" + ), + 'model': 'glm-4', + 'usage': {'prompt_tokens': 100, 'completion_tokens': 50} + } + ) + mock_get_glm_service.return_value = mock_service + + # Очищаем базу перед тестом + BouquetName.objects.all().delete() + + # Вызываем метод генерации и сохранения + success, msg, data = self.generator.generate_and_store(count=3) + + self.assertTrue(success) + self.assertIn("Сгенерировано и сохранено 3 названий для букетов", msg) + self.assertEqual(BouquetName.objects.count(), 3) + + def test_mark_as_used(self): + """ + Тест увеличения счетчика использования названия + """ + # Создаем тестовое название + bouquet_name = BouquetName.objects.create( + name="Тестовый букет", + language="russian", + is_approved=True + ) + + # Проверяем начальное значение счетчика + self.assertEqual(bouquet_name.usage_count, 0) + + # Увеличиваем счетчик + self.generator.mark_as_used("Тестовый букет", "russian") + + # Проверяем обновленное значение + bouquet_name.refresh_from_db() + self.assertEqual(bouquet_name.usage_count, 1) + + def test_get_approved_names(self): + """ + Тест получения одобренных названий + """ + # Очищаем базу перед тестом + BouquetName.objects.all().delete() + + # Создаем тестовые данные + BouquetName.objects.create( + name="Одобренный букет 1", + language="russian", + is_approved=True + ) + BouquetName.objects.create( + name="Одобренный букет 2", + language="russian", + is_approved=True + ) + BouquetName.objects.create( + name="Неодобренный букет", + language="russian", + is_approved=False + ) + + # Получаем одобренные названия + approved_names = self.generator.get_approved_names(language="russian") + + self.assertEqual(len(approved_names), 2) + self.assertIn("Одобренный букет 1", approved_names) + self.assertIn("Одобренный букет 2", approved_names) + self.assertNotIn("Неодобренный букет", approved_names) + + def test_bouquet_name_model(self): + """ + Тест создания и работы с моделью BouquetName + """ + # Создаем тестовые теги + red_tag = ProductTag.objects.create(name="красный", slug="krasny") + romantic_tag = ProductTag.objects.create(name="романтический", slug="romanticheskiy") + + # Создаем экземпляр модели + bouquet_name = BouquetName.objects.create( + name="Романтический букет", + language="russian", + is_approved=True + ) + + # Добавляем теги + bouquet_name.color_tags.add(red_tag) + bouquet_name.style_tags.add(romantic_tag) + + # Проверяем сохраненные значения + self.assertEqual(bouquet_name.name, "Романтический букет") + self.assertEqual(bouquet_name.language, "russian") + self.assertTrue(bouquet_name.is_approved) + self.assertEqual(bouquet_name.usage_count, 0) + self.assertIn(red_tag, bouquet_name.color_tags.all()) + self.assertIn(romantic_tag, bouquet_name.style_tags.all()) + + # Обновляем поле + bouquet_name.usage_count = 5 + bouquet_name.save() + + # Проверяем обновление + updated_name = BouquetName.objects.get(id=bouquet_name.id) + self.assertEqual(updated_name.usage_count, 5) diff --git a/myproject/products/urls.py b/myproject/products/urls.py index 82f5022..b60f8ff 100644 --- a/myproject/products/urls.py +++ b/myproject/products/urls.py @@ -56,6 +56,8 @@ urlpatterns = [ path('api/payment-methods/', api_views.get_payment_methods, name='api-payment-methods'), path('api/filtered-items-ids/', api_views.get_filtered_items_ids, name='api-filtered-items-ids'), 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/generate/', api_views.GenerateBouquetNamesView.as_view(), name='api-generate-bouquet-names'), # Photo processing status API (for AJAX polling) path('api/photos/status//', photo_status_api.photo_processing_status, name='api-photo-status'), diff --git a/myproject/products/views/__init__.py b/myproject/products/views/__init__.py index b372e4c..5ecaead 100644 --- a/myproject/products/views/__init__.py +++ b/myproject/products/views/__init__.py @@ -115,7 +115,14 @@ from .attribute_views import ( ) # API представления -from .api_views import search_products_and_variants, validate_kit_cost, create_temporary_kit_api, create_tag_api +from .api_views import ( + search_products_and_variants, + validate_kit_cost, + create_temporary_kit_api, + create_tag_api, + RandomBouquetNamesView, + GenerateBouquetNamesView, +) # Каталог from .catalog_views import CatalogView @@ -227,6 +234,8 @@ __all__ = [ 'validate_kit_cost', 'create_temporary_kit_api', 'create_tag_api', + 'RandomBouquetNamesView', + 'GenerateBouquetNamesView', # Каталог 'CatalogView', diff --git a/myproject/products/views/api_views.py b/myproject/products/views/api_views.py index badaacd..472b080 100644 --- a/myproject/products/views/api_views.py +++ b/myproject/products/views/api_views.py @@ -1800,3 +1800,46 @@ def bulk_update_categories(request): 'success': False, 'message': f'Произошла ошибка: {str(e)}' }, status=500) + + +# ========== Генератор названий букетов ========== + +from django.views import View +from ..models import BouquetName +from ..services import BouquetNameGenerator + + +class RandomBouquetNamesView(View): + """Возвращает случайные названия из базы""" + + def get(self, request): + count = int(request.GET.get('count', 3)) + # Ограничиваем максимум до 100 + count = min(count, 100) + names = list(BouquetName.objects.order_by('?')[:count].values_list('name', flat=True)) + return JsonResponse({'names': names}) + + +class GenerateBouquetNamesView(View): + """Генерирует новые названия через LLM и сохраняет в базу""" + + def post(self, request): + count = int(request.POST.get('count', 10)) + # Ограничиваем максимум до 500 + count = min(count, 500) + + generator = BouquetNameGenerator() + + success, msg, data = generator.generate_and_store( + count=count, + language='russian' + ) + + if success: + return JsonResponse({ + 'success': True, + 'message': msg, + 'count': len(data.get('names', [])) + }) + else: + return JsonResponse({'success': False, 'error': msg}, status=400) diff --git a/myproject/products/views/productkit_views.py b/myproject/products/views/productkit_views.py index e26be53..9cd5553 100644 --- a/myproject/products/views/productkit_views.py +++ b/myproject/products/views/productkit_views.py @@ -9,7 +9,7 @@ from django.shortcuts import redirect from django.db import transaction, IntegrityError from user_roles.mixins import ManagerOwnerRequiredMixin -from ..models import ProductKit, ProductCategory, ProductTag, ProductKitPhoto, Product, ProductVariantGroup +from ..models import ProductKit, ProductCategory, ProductTag, ProductKitPhoto, Product, ProductVariantGroup, BouquetName from ..forms import ProductKitForm, KitItemFormSetCreate, KitItemFormSetUpdate from .utils import handle_photos @@ -197,6 +197,9 @@ class ProductKitCreateView(LoginRequiredMixin, ManagerOwnerRequiredMixin, Create else: context['kititem_formset'] = KitItemFormSetCreate(prefix='kititem') + # Количество названий букетов в базе + context['bouquet_names_count'] = BouquetName.objects.count() + return context def form_valid(self, form):