Feat: Add inline category creation in catalog with clickable product names
Added inline category creation functionality to catalog page with user-friendly interface: - Inline input fields for creating root and nested categories - '+' button in category tree header for root categories - '+' icon on hover for each category node to create subcategories - Clickable product/kit names in catalog grid and list views - AJAX API endpoint for category creation with validation - User-friendly error messages for duplicate names and constraints - Clean implementation using clearTimeout pattern to prevent duplicate requests Technical details: - New API endpoint: POST /products/api/categories/create/ - Auto-generates slug and SKU for new categories - Validates uniqueness, parent existence, and circular references - Single-request submission with proper race condition handling - Removes debug logging for production-ready code 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -942,3 +942,142 @@ def rename_category_api(request, pk):
|
||||
'success': False,
|
||||
'error': f'Ошибка при переименовании: {str(e)}'
|
||||
}, status=500)
|
||||
|
||||
|
||||
def create_category_api(request):
|
||||
"""
|
||||
AJAX endpoint для создания новой категории (inline creation).
|
||||
|
||||
Принимает JSON:
|
||||
{
|
||||
"name": "Название категории",
|
||||
"parent_id": 123 # опционально, null для корневой категории
|
||||
}
|
||||
|
||||
Возвращает JSON:
|
||||
{
|
||||
"success": true,
|
||||
"category": {
|
||||
"id": 456,
|
||||
"name": "Название категории",
|
||||
"slug": "nazvanie-kategorii",
|
||||
"parent_id": 123 или null
|
||||
}
|
||||
}
|
||||
"""
|
||||
if request.method != 'POST':
|
||||
return JsonResponse({
|
||||
'success': False,
|
||||
'error': 'Метод не поддерживается'
|
||||
}, status=405)
|
||||
|
||||
try:
|
||||
import json
|
||||
from django.db import IntegrityError
|
||||
|
||||
data = json.loads(request.body)
|
||||
name = data.get('name', '').strip()
|
||||
parent_id = data.get('parent_id')
|
||||
|
||||
# Валидация названия
|
||||
if not name:
|
||||
return JsonResponse({
|
||||
'success': False,
|
||||
'error': 'Название категории не может быть пустым'
|
||||
}, status=400)
|
||||
|
||||
if len(name) > 200:
|
||||
return JsonResponse({
|
||||
'success': False,
|
||||
'error': 'Название слишком длинное (максимум 200 символов)'
|
||||
}, status=400)
|
||||
|
||||
# Получаем родительскую категорию, если указана
|
||||
parent = None
|
||||
if parent_id:
|
||||
try:
|
||||
parent = ProductCategory.objects.get(pk=parent_id)
|
||||
if not parent.is_active:
|
||||
return JsonResponse({
|
||||
'success': False,
|
||||
'error': 'Родительская категория неактивна'
|
||||
}, status=400)
|
||||
except ProductCategory.DoesNotExist:
|
||||
return JsonResponse({
|
||||
'success': False,
|
||||
'error': 'Родительская категория не найдена'
|
||||
}, status=404)
|
||||
|
||||
# Создаем новую категорию
|
||||
category = ProductCategory(
|
||||
name=name,
|
||||
parent=parent,
|
||||
is_active=True
|
||||
)
|
||||
|
||||
# save() автоматически сгенерирует slug и sku
|
||||
category.save()
|
||||
|
||||
return JsonResponse({
|
||||
'success': True,
|
||||
'category': {
|
||||
'id': category.pk,
|
||||
'name': category.name,
|
||||
'slug': category.slug,
|
||||
'parent_id': category.parent_id
|
||||
}
|
||||
})
|
||||
|
||||
except IntegrityError as e:
|
||||
# Определяем тип ошибки уникальности
|
||||
error_str = str(e).lower()
|
||||
if 'unique_active_category_name' in error_str or 'unique constraint' in error_str:
|
||||
error_message = 'Категория с таким названием уже существует'
|
||||
elif 'sku' in error_str:
|
||||
error_message = 'Ошибка при генерации артикула. Попробуйте ещё раз'
|
||||
else:
|
||||
error_message = 'Ошибка: категория с такими данными уже существует'
|
||||
|
||||
return JsonResponse({
|
||||
'success': False,
|
||||
'error': error_message
|
||||
}, status=400)
|
||||
except json.JSONDecodeError:
|
||||
return JsonResponse({
|
||||
'success': False,
|
||||
'error': 'Некорректный JSON'
|
||||
}, status=400)
|
||||
except ValidationError as e:
|
||||
# ValidationError может содержать словарь с ошибками
|
||||
if hasattr(e, 'message_dict'):
|
||||
# Извлекаем первое сообщение из словаря
|
||||
error_messages = []
|
||||
for field, messages in e.message_dict.items():
|
||||
if isinstance(messages, list):
|
||||
error_messages.extend(messages)
|
||||
else:
|
||||
error_messages.append(str(messages))
|
||||
error_text = ' '.join(error_messages)
|
||||
elif hasattr(e, 'messages'):
|
||||
error_text = ' '.join(e.messages)
|
||||
else:
|
||||
error_text = str(e)
|
||||
|
||||
# Заменяем технические сообщения на понятные
|
||||
if 'unique_active_category_name' in error_text.lower():
|
||||
error_text = 'Категория с таким названием уже существует'
|
||||
elif 'циклическая ссылка' in error_text.lower():
|
||||
error_text = 'Невозможно создать категорию: обнаружена циклическая ссылка'
|
||||
elif 'слишком глубокая вложенность' in error_text.lower():
|
||||
error_text = 'Превышена максимальная глубина вложенности категорий'
|
||||
|
||||
return JsonResponse({
|
||||
'success': False,
|
||||
'error': error_text
|
||||
}, status=400)
|
||||
except Exception as e:
|
||||
logger.error(f'Ошибка при создании категории: {str(e)}')
|
||||
return JsonResponse({
|
||||
'success': False,
|
||||
'error': f'Ошибка при создании категории: {str(e)}'
|
||||
}, status=500)
|
||||
|
||||
Reference in New Issue
Block a user