Основные изменения: - Переименование ConfigurableKitProduct → ConfigurableProduct - Добавлена поддержка Product как варианта (не только ProductKit) - Создан справочник атрибутов (ProductAttribute, ProductAttributeValue) - CRUD для управления атрибутами с inline редактированием значений - Пересозданы миграции с нуля для всех приложений - Добавлена ссылка на атрибуты в навигацию 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
248 lines
9.2 KiB
Python
248 lines
9.2 KiB
Python
"""
|
||
CRUD представления для справочника атрибутов товаров (ProductAttribute, ProductAttributeValue).
|
||
"""
|
||
from django.contrib import messages
|
||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||
from django.views.generic import ListView, CreateView, DetailView, UpdateView, DeleteView
|
||
from django.urls import reverse_lazy
|
||
from django.db.models import Q, Count
|
||
from django.db import IntegrityError
|
||
from django.http import JsonResponse
|
||
from django.views.decorators.http import require_POST
|
||
from django.contrib.auth.decorators import login_required
|
||
import json
|
||
|
||
from ..models import ProductAttribute, ProductAttributeValue
|
||
from ..forms import ProductAttributeForm, ProductAttributeValueFormSet
|
||
|
||
|
||
class ProductAttributeListView(LoginRequiredMixin, ListView):
|
||
"""Список всех атрибутов с поиском"""
|
||
model = ProductAttribute
|
||
template_name = 'products/attribute_list.html'
|
||
context_object_name = 'attributes'
|
||
paginate_by = 20
|
||
|
||
def get_queryset(self):
|
||
queryset = super().get_queryset()
|
||
|
||
# Аннотируем количество значений для каждого атрибута
|
||
queryset = queryset.annotate(
|
||
num_values=Count('values', distinct=True)
|
||
)
|
||
|
||
# Поиск по названию и slug
|
||
search_query = self.request.GET.get('search')
|
||
if search_query:
|
||
queryset = queryset.filter(
|
||
Q(name__icontains=search_query) |
|
||
Q(slug__icontains=search_query)
|
||
)
|
||
|
||
return queryset.order_by('position', 'name')
|
||
|
||
def get_context_data(self, **kwargs):
|
||
context = super().get_context_data(**kwargs)
|
||
context['search_query'] = self.request.GET.get('search', '')
|
||
return context
|
||
|
||
|
||
class ProductAttributeDetailView(LoginRequiredMixin, DetailView):
|
||
"""Детальная информация об атрибуте с его значениями"""
|
||
model = ProductAttribute
|
||
template_name = 'products/attribute_detail.html'
|
||
context_object_name = 'attribute'
|
||
|
||
def get_context_data(self, **kwargs):
|
||
context = super().get_context_data(**kwargs)
|
||
attribute = self.get_object()
|
||
|
||
# Получаем все значения атрибута
|
||
context['values'] = attribute.values.all().order_by('position', 'value')
|
||
|
||
return context
|
||
|
||
|
||
class ProductAttributeCreateView(LoginRequiredMixin, CreateView):
|
||
"""Создание нового атрибута с inline значениями"""
|
||
model = ProductAttribute
|
||
form_class = ProductAttributeForm
|
||
template_name = 'products/attribute_form.html'
|
||
success_url = reverse_lazy('products:attribute-list')
|
||
|
||
def get_context_data(self, **kwargs):
|
||
context = super().get_context_data(**kwargs)
|
||
if self.request.POST:
|
||
context['value_formset'] = ProductAttributeValueFormSet(self.request.POST, instance=self.object)
|
||
else:
|
||
context['value_formset'] = ProductAttributeValueFormSet(instance=self.object)
|
||
return context
|
||
|
||
def form_valid(self, form):
|
||
context = self.get_context_data()
|
||
value_formset = context['value_formset']
|
||
|
||
try:
|
||
self.object = form.save()
|
||
|
||
if value_formset.is_valid():
|
||
value_formset.instance = self.object
|
||
value_formset.save()
|
||
else:
|
||
return self.form_invalid(form)
|
||
|
||
messages.success(self.request, f'Атрибут "{self.object.name}" успешно создан.')
|
||
return super().form_valid(form)
|
||
|
||
except IntegrityError as e:
|
||
error_msg = str(e).lower()
|
||
if 'unique' in error_msg:
|
||
messages.error(
|
||
self.request,
|
||
f'Ошибка: атрибут с таким названием уже существует.'
|
||
)
|
||
else:
|
||
messages.error(
|
||
self.request,
|
||
'Ошибка при сохранении атрибута. Пожалуйста, проверьте введённые данные.'
|
||
)
|
||
return self.form_invalid(form)
|
||
|
||
|
||
class ProductAttributeUpdateView(LoginRequiredMixin, UpdateView):
|
||
"""Редактирование существующего атрибута с inline значениями"""
|
||
model = ProductAttribute
|
||
form_class = ProductAttributeForm
|
||
template_name = 'products/attribute_form.html'
|
||
success_url = reverse_lazy('products:attribute-list')
|
||
|
||
def get_context_data(self, **kwargs):
|
||
context = super().get_context_data(**kwargs)
|
||
if self.request.POST:
|
||
context['value_formset'] = ProductAttributeValueFormSet(self.request.POST, instance=self.object)
|
||
else:
|
||
context['value_formset'] = ProductAttributeValueFormSet(instance=self.object)
|
||
return context
|
||
|
||
def form_valid(self, form):
|
||
context = self.get_context_data()
|
||
value_formset = context['value_formset']
|
||
|
||
try:
|
||
self.object = form.save()
|
||
|
||
if value_formset.is_valid():
|
||
value_formset.save()
|
||
else:
|
||
return self.form_invalid(form)
|
||
|
||
messages.success(self.request, f'Атрибут "{self.object.name}" успешно обновлен.')
|
||
return super().form_valid(form)
|
||
|
||
except IntegrityError as e:
|
||
error_msg = str(e).lower()
|
||
if 'unique' in error_msg:
|
||
messages.error(
|
||
self.request,
|
||
f'Ошибка: атрибут с таким названием уже существует.'
|
||
)
|
||
else:
|
||
messages.error(
|
||
self.request,
|
||
'Ошибка при сохранении атрибута. Пожалуйста, проверьте введённые данные.'
|
||
)
|
||
return self.form_invalid(form)
|
||
|
||
|
||
class ProductAttributeDeleteView(LoginRequiredMixin, DeleteView):
|
||
"""Удаление атрибута с подтверждением"""
|
||
model = ProductAttribute
|
||
template_name = 'products/attribute_confirm_delete.html'
|
||
success_url = reverse_lazy('products:attribute-list')
|
||
|
||
def get_context_data(self, **kwargs):
|
||
context = super().get_context_data(**kwargs)
|
||
attribute = self.get_object()
|
||
|
||
# Количество значений
|
||
context['values_count'] = attribute.values.count()
|
||
|
||
return context
|
||
|
||
def delete(self, request, *args, **kwargs):
|
||
attribute = self.get_object()
|
||
attribute_name = attribute.name
|
||
response = super().delete(request, *args, **kwargs)
|
||
messages.success(request, f'Атрибут "{attribute_name}" успешно удален.')
|
||
return response
|
||
|
||
|
||
# API endpoints
|
||
|
||
@login_required
|
||
@require_POST
|
||
def create_attribute_api(request):
|
||
"""API для быстрого создания атрибута"""
|
||
try:
|
||
data = json.loads(request.body)
|
||
name = data.get('name', '').strip()
|
||
|
||
if not name:
|
||
return JsonResponse({'success': False, 'error': 'Название обязательно'})
|
||
|
||
attribute = ProductAttribute.objects.create(name=name)
|
||
return JsonResponse({
|
||
'success': True,
|
||
'id': attribute.pk,
|
||
'name': attribute.name,
|
||
'slug': attribute.slug
|
||
})
|
||
except IntegrityError:
|
||
return JsonResponse({'success': False, 'error': 'Атрибут с таким названием уже существует'})
|
||
except Exception as e:
|
||
return JsonResponse({'success': False, 'error': str(e)})
|
||
|
||
|
||
@login_required
|
||
@require_POST
|
||
def add_attribute_value_api(request, pk):
|
||
"""API для добавления значения к атрибуту"""
|
||
try:
|
||
data = json.loads(request.body)
|
||
value = data.get('value', '').strip()
|
||
|
||
if not value:
|
||
return JsonResponse({'success': False, 'error': 'Значение обязательно'})
|
||
|
||
attribute = ProductAttribute.objects.get(pk=pk)
|
||
attr_value = ProductAttributeValue.objects.create(
|
||
attribute=attribute,
|
||
value=value
|
||
)
|
||
return JsonResponse({
|
||
'success': True,
|
||
'id': attr_value.pk,
|
||
'value': attr_value.value,
|
||
'slug': attr_value.slug
|
||
})
|
||
except ProductAttribute.DoesNotExist:
|
||
return JsonResponse({'success': False, 'error': 'Атрибут не найден'})
|
||
except IntegrityError:
|
||
return JsonResponse({'success': False, 'error': 'Такое значение уже существует'})
|
||
except Exception as e:
|
||
return JsonResponse({'success': False, 'error': str(e)})
|
||
|
||
|
||
@login_required
|
||
@require_POST
|
||
def delete_attribute_value_api(request, pk, value_id):
|
||
"""API для удаления значения атрибута"""
|
||
try:
|
||
value = ProductAttributeValue.objects.get(pk=value_id, attribute_id=pk)
|
||
value.delete()
|
||
return JsonResponse({'success': True})
|
||
except ProductAttributeValue.DoesNotExist:
|
||
return JsonResponse({'success': False, 'error': 'Значение не найдено'})
|
||
except Exception as e:
|
||
return JsonResponse({'success': False, 'error': str(e)})
|