Реализован полный CRUD для тегов товаров

Упрощена модель ProductTag:
- Удалены поля soft delete (is_deleted, deleted_at, deleted_by)
- Добавлено поле is_active для управления статусом
- Упрощены менеджеры и методы модели

Создан CRUD функционал:
- ProductTagForm: форма с автогенерацией slug
- Views: список, создание, просмотр, редактирование, удаление
- URL маршруты: /products/tags/*
- Шаблоны: list, form, detail, confirm_delete

Особенности:
- Поиск по названию и slug
- Фильтрация по статусу активности
- Статистика использования тегов в товарах/комплектах
- Пагинация (20 на страницу)
- Предупреждение при удалении с отображением связанных объектов
- Добавлена ссылка "Теги" в навигацию

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-11 23:14:01 +03:00
parent 4a1f8266de
commit 1a0360f8c0
12 changed files with 733 additions and 48 deletions

View File

@@ -70,6 +70,15 @@ from .variant_group_views import (
product_variant_group_item_move,
)
# CRUD представления для ProductTag
from .tag_views import (
ProductTagListView,
ProductTagCreateView,
ProductTagDetailView,
ProductTagUpdateView,
ProductTagDeleteView,
)
# API представления
from .api_views import search_products_and_variants, validate_kit_cost, create_temporary_kit_api
@@ -129,6 +138,13 @@ __all__ = [
'ProductVariantGroupDeleteView',
'product_variant_group_item_move',
# ProductTag CRUD
'ProductTagListView',
'ProductTagCreateView',
'ProductTagDetailView',
'ProductTagUpdateView',
'ProductTagDeleteView',
# API
'search_products_and_variants',
'validate_kit_cost',

View File

@@ -0,0 +1,120 @@
"""
CRUD представления для тегов товаров (ProductTag).
"""
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 ..models import ProductTag
from ..forms import ProductTagForm
class ProductTagListView(LoginRequiredMixin, ListView):
"""Список всех тегов с поиском и фильтрацией"""
model = ProductTag
template_name = 'products/tag_list.html'
context_object_name = 'tags'
paginate_by = 20
def get_queryset(self):
queryset = super().get_queryset()
# Аннотируем количество товаров и комплектов для каждого тега
queryset = queryset.annotate(
products_count=Count('products', distinct=True),
kits_count=Count('kits', 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)
)
# Фильтр по статусу активности
is_active = self.request.GET.get('is_active')
if is_active == '1':
queryset = queryset.filter(is_active=True)
elif is_active == '0':
queryset = queryset.filter(is_active=False)
return queryset.order_by('name')
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['search_query'] = self.request.GET.get('search', '')
context['is_active_filter'] = self.request.GET.get('is_active', '')
return context
class ProductTagDetailView(LoginRequiredMixin, DetailView):
"""Детальная информация о теге с привязанными товарами и комплектами"""
model = ProductTag
template_name = 'products/tag_detail.html'
context_object_name = 'tag'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
tag = self.get_object()
# Получаем товары и комплекты с этим тегом
context['products'] = tag.products.filter(is_active=True).order_by('name')[:20]
context['kits'] = tag.kits.filter(is_active=True).order_by('name')[:20]
context['total_products'] = tag.products.count()
context['total_kits'] = tag.kits.count()
return context
class ProductTagCreateView(LoginRequiredMixin, CreateView):
"""Создание нового тега"""
model = ProductTag
form_class = ProductTagForm
template_name = 'products/tag_form.html'
success_url = reverse_lazy('products:tag-list')
def form_valid(self, form):
response = super().form_valid(form)
messages.success(self.request, f'Тег "{self.object.name}" успешно создан.')
return response
class ProductTagUpdateView(LoginRequiredMixin, UpdateView):
"""Редактирование существующего тега"""
model = ProductTag
form_class = ProductTagForm
template_name = 'products/tag_form.html'
success_url = reverse_lazy('products:tag-list')
def form_valid(self, form):
response = super().form_valid(form)
messages.success(self.request, f'Тег "{self.object.name}" успешно обновлен.')
return response
class ProductTagDeleteView(LoginRequiredMixin, DeleteView):
"""Удаление тега с подтверждением"""
model = ProductTag
template_name = 'products/tag_confirm_delete.html'
success_url = reverse_lazy('products:tag-list')
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
tag = self.get_object()
# Передаем информацию о связанных объектах для предупреждения
context['products_count'] = tag.products.count()
context['kits_count'] = tag.kits.count()
return context
def delete(self, request, *args, **kwargs):
tag = self.get_object()
tag_name = tag.name
response = super().delete(request, *args, **kwargs)
messages.success(request, f'Тег "{tag_name}" успешно удален.')
return response