Реализован полный 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

@@ -360,29 +360,10 @@ class ProductCategoryAdmin(admin.ModelAdmin):
class ProductTagAdmin(admin.ModelAdmin):
list_display = ('name', 'slug', 'get_deleted_status')
list_filter = (DeletedFilter,)
list_display = ('name', 'slug', 'is_active')
list_filter = ('is_active',)
prepopulated_fields = {'slug': ('name',)}
search_fields = ('name',)
readonly_fields = ('deleted_at', 'deleted_by')
actions = [restore_items, delete_selected, hard_delete_selected]
def get_queryset(self, request):
"""Переопределяем queryset для доступа ко всем тегам (включая удаленные)"""
qs = ProductTag.all_objects.all()
ordering = self.get_ordering(request)
if ordering:
qs = qs.order_by(*ordering)
return qs
def get_deleted_status(self, obj):
"""Показывает статус удаления"""
if obj.is_deleted:
return format_html(
'<span style="color: red; font-weight: bold;">🗑️ Удален</span>'
)
return format_html('<span style="color: green;">✓ Активен</span>')
get_deleted_status.short_description = 'Статус'
class ProductAdmin(admin.ModelAdmin):

View File

@@ -451,3 +451,40 @@ ProductVariantGroupItemFormSetUpdate = inlineformset_factory(
validate_min=False, # Не требовать минимум товаров
can_delete_extra=True, # Разрешить удалять дополнительные формы
)
class ProductTagForm(forms.ModelForm):
"""
Форма для создания и редактирования тегов товаров.
"""
class Meta:
model = ProductTag
fields = ['name', 'slug', 'is_active']
labels = {
'name': 'Название',
'slug': 'URL-идентификатор',
'is_active': 'Активен'
}
help_texts = {
'slug': 'Оставьте пустым для автоматической генерации из названия',
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['name'].widget.attrs.update({
'class': 'form-control form-control-lg fw-semibold',
'placeholder': 'Введите название тега'
})
self.fields['slug'].widget.attrs.update({
'class': 'form-control',
'placeholder': 'url-identifier (автоматически)'
})
self.fields['slug'].required = False
self.fields['is_active'].widget.attrs.update({'class': 'form-check-input'})
def clean_slug(self):
"""Разрешаем пустой slug - он сгенерируется в модели"""
slug = self.cleaned_data.get('slug')
if slug == '' or slug is None:
return None
return slug

View File

@@ -0,0 +1,42 @@
# Generated by Django 5.0.10 on 2025-11-11 18:31
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('products', '0001_initial'),
]
operations = [
migrations.RemoveIndex(
model_name='producttag',
name='products_pr_is_dele_ea9be0_idx',
),
migrations.RemoveIndex(
model_name='producttag',
name='products_pr_is_dele_bc2d9c_idx',
),
migrations.RemoveField(
model_name='producttag',
name='deleted_at',
),
migrations.RemoveField(
model_name='producttag',
name='deleted_by',
),
migrations.RemoveField(
model_name='producttag',
name='is_deleted',
),
migrations.AddField(
model_name='producttag',
name='is_active',
field=models.BooleanField(db_index=True, default=True, verbose_name='Активен'),
),
migrations.AddIndex(
model_name='producttag',
index=models.Index(fields=['is_active'], name='products_pr_is_acti_7f288f_idx'),
),
]

View File

@@ -129,30 +129,18 @@ class ProductTag(models.Model):
"""
name = models.CharField(max_length=100, unique=True, verbose_name="Название")
slug = models.SlugField(max_length=100, unique=True, verbose_name="URL-идентификатор")
is_active = models.BooleanField(default=True, verbose_name="Активен", db_index=True)
created_at = models.DateTimeField(auto_now_add=True, verbose_name="Дата создания", null=True)
updated_at = models.DateTimeField(auto_now=True, verbose_name="Дата обновления", null=True)
# Поля для мягкого удаления
is_deleted = models.BooleanField(default=False, verbose_name="Удален", db_index=True)
deleted_at = models.DateTimeField(null=True, blank=True, verbose_name="Время удаления")
deleted_by = models.ForeignKey(
User,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='deleted_tags',
verbose_name="Удален пользователем"
)
objects = SoftDeleteManager.from_queryset(SoftDeleteQuerySet)() # Менеджер по умолчанию (исключает удаленные)
all_objects = models.Manager() # Менеджер для доступа ко ВСЕМ объектам (включая удаленные)
objects = models.Manager()
active = ActiveManager()
class Meta:
verbose_name = "Тег товара"
verbose_name_plural = "Теги товаров"
indexes = [
models.Index(fields=['is_deleted']),
models.Index(fields=['is_deleted', 'created_at']),
models.Index(fields=['is_active']),
]
def __str__(self):
@@ -162,14 +150,3 @@ class ProductTag(models.Model):
if not self.slug:
self.slug = SlugService.generate_unique_slug(self.name, ProductTag, self.pk)
super().save(*args, **kwargs)
def delete(self, *args, **kwargs):
"""Soft delete вместо hard delete - марк как удаленный"""
self.is_deleted = True
self.deleted_at = timezone.now()
self.save(update_fields=['is_deleted', 'deleted_at'])
return 1, {self.__class__._meta.label: 1}
def hard_delete(self):
"""Полное удаление из БД (необратимо!)"""
super().delete()

View File

@@ -0,0 +1,81 @@
{% extends 'base.html' %}
{% block title %}Удалить тег: {{ tag.name }}{% endblock %}
{% block content %}
<div class="container mt-5">
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card border-danger">
<div class="card-header bg-danger text-white">
<h4 class="mb-0">
<i class="bi bi-exclamation-triangle"></i> Подтверждение удаления
</h4>
</div>
<div class="card-body">
<p class="lead">
Вы уверены, что хотите удалить тег <strong>"{{ tag.name }}"</strong>?
</p>
{% if products_count > 0 or kits_count > 0 %}
<div class="alert alert-warning" role="alert">
<h6 class="alert-heading">
<i class="bi bi-exclamation-circle"></i> Внимание!
</h6>
<p class="mb-2">Этот тег используется в:</p>
<ul class="mb-0">
{% if products_count > 0 %}
<li><strong>{{ products_count }}</strong> товарах</li>
{% endif %}
{% if kits_count > 0 %}
<li><strong>{{ kits_count }}</strong> комплектах</li>
{% endif %}
</ul>
<hr>
<p class="mb-0 small">
При удалении тега он будет удален из всех связанных товаров и комплектов.
</p>
</div>
{% else %}
<div class="alert alert-info" role="alert">
<i class="bi bi-info-circle"></i> Этот тег не используется в товарах или комплектах.
</div>
{% endif %}
<form method="post">
{% csrf_token %}
<div class="d-grid gap-2 d-md-flex justify-content-md-end mt-4">
<a href="{% url 'products:tag-detail' tag.pk %}" class="btn btn-secondary">
<i class="bi bi-x-circle"></i> Отмена
</a>
<button type="submit" class="btn btn-danger">
<i class="bi bi-trash"></i> Да, удалить тег
</button>
</div>
</form>
</div>
</div>
<!-- Дополнительная информация -->
<div class="card mt-3">
<div class="card-body">
<h6 class="card-title">Информация о теге</h6>
<dl class="row mb-0 small">
<dt class="col-sm-4">Название:</dt>
<dd class="col-sm-8">{{ tag.name }}</dd>
<dt class="col-sm-4">Slug:</dt>
<dd class="col-sm-8"><code>{{ tag.slug }}</code></dd>
<dt class="col-sm-4">Создан:</dt>
<dd class="col-sm-8">{{ tag.created_at|date:"d.m.Y H:i" }}</dd>
<dt class="col-sm-4">Обновлен:</dt>
<dd class="col-sm-8">{{ tag.updated_at|date:"d.m.Y H:i" }}</dd>
</dl>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,175 @@
{% extends 'base.html' %}
{% block title %}Тег: {{ tag.name }}{% endblock %}
{% block content %}
<div class="container mt-5">
<!-- Заголовок с кнопками действий -->
<div class="d-flex justify-content-between align-items-center mb-4">
<h2>
<i class="bi bi-tag"></i> {{ tag.name }}
{% if not tag.is_active %}
<span class="badge bg-secondary">Неактивен</span>
{% endif %}
</h2>
<div class="btn-group" role="group">
<a href="{% url 'products:tag-update' tag.pk %}" class="btn btn-warning">
<i class="bi bi-pencil"></i> Изменить
</a>
<a href="{% url 'products:tag-delete' tag.pk %}" class="btn btn-danger">
<i class="bi bi-trash"></i> Удалить
</a>
<a href="{% url 'products:tag-list' %}" class="btn btn-outline-secondary">
<i class="bi bi-arrow-left"></i> К списку
</a>
</div>
</div>
<!-- Основная информация -->
<div class="row">
<div class="col-md-4">
<div class="card mb-4">
<div class="card-header">
<h5 class="mb-0">Информация о теге</h5>
</div>
<div class="card-body">
<dl class="row mb-0">
<dt class="col-sm-4">Название:</dt>
<dd class="col-sm-8">{{ tag.name }}</dd>
<dt class="col-sm-4">Slug:</dt>
<dd class="col-sm-8"><code>{{ tag.slug }}</code></dd>
<dt class="col-sm-4">Статус:</dt>
<dd class="col-sm-8">
{% if tag.is_active %}
<span class="badge bg-success">Активен</span>
{% else %}
<span class="badge bg-secondary">Неактивен</span>
{% endif %}
</dd>
<dt class="col-sm-4">Создан:</dt>
<dd class="col-sm-8">{{ tag.created_at|date:"d.m.Y H:i" }}</dd>
<dt class="col-sm-4">Обновлен:</dt>
<dd class="col-sm-8">{{ tag.updated_at|date:"d.m.Y H:i" }}</dd>
</dl>
</div>
</div>
<!-- Статистика -->
<div class="card">
<div class="card-header">
<h5 class="mb-0">Статистика</h5>
</div>
<div class="card-body">
<dl class="row mb-0">
<dt class="col-sm-6">Товаров:</dt>
<dd class="col-sm-6">
<span class="badge bg-info">{{ total_products }}</span>
</dd>
<dt class="col-sm-6">Комплектов:</dt>
<dd class="col-sm-6">
<span class="badge bg-info">{{ total_kits }}</span>
</dd>
</dl>
</div>
</div>
</div>
<!-- Товары с этим тегом -->
<div class="col-md-8">
<div class="card mb-4">
<div class="card-header">
<h5 class="mb-0">Товары с тегом "{{ tag.name }}"</h5>
</div>
<div class="card-body">
{% if products %}
<div class="table-responsive">
<table class="table table-sm table-hover">
<thead>
<tr>
<th>Название</th>
<th>Артикул</th>
<th>Цена</th>
<th>Действия</th>
</tr>
</thead>
<tbody>
{% for product in products %}
<tr>
<td>{{ product.name }}</td>
<td><code>{{ product.sku }}</code></td>
<td>{{ product.actual_price|floatformat:0 }} руб.</td>
<td>
<a href="{% url 'products:product-detail' product.pk %}"
class="btn btn-sm btn-outline-primary">
Просмотр
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% if total_products > 20 %}
<div class="alert alert-info mb-0">
Показано первых 20 из {{ total_products }} товаров.
</div>
{% endif %}
{% else %}
<p class="text-muted mb-0">Нет товаров с этим тегом</p>
{% endif %}
</div>
</div>
<!-- Комплекты с этим тегом -->
<div class="card">
<div class="card-header">
<h5 class="mb-0">Комплекты с тегом "{{ tag.name }}"</h5>
</div>
<div class="card-body">
{% if kits %}
<div class="table-responsive">
<table class="table table-sm table-hover">
<thead>
<tr>
<th>Название</th>
<th>Артикул</th>
<th>Цена</th>
<th>Действия</th>
</tr>
</thead>
<tbody>
{% for kit in kits %}
<tr>
<td>{{ kit.name }}</td>
<td><code>{{ kit.sku }}</code></td>
<td>{{ kit.get_sale_price|floatformat:0 }} руб.</td>
<td>
<a href="{% url 'products:productkit-detail' kit.pk %}"
class="btn btn-sm btn-outline-primary">
Просмотр
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% if total_kits > 20 %}
<div class="alert alert-info mb-0">
Показано первых 20 из {{ total_kits }} комплектов.
</div>
{% endif %}
{% else %}
<p class="text-muted mb-0">Нет комплектов с этим тегом</p>
{% endif %}
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,99 @@
{% extends 'base.html' %}
{% block title %}{% if object %}Редактировать тег{% else %}Создать тег{% endif %}{% endblock %}
{% block content %}
<div class="container mt-5">
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card">
<div class="card-header">
<h4 class="mb-0">
{% if object %}
<i class="bi bi-pencil"></i> Редактировать тег
{% else %}
<i class="bi bi-plus-circle"></i> Создать новый тег
{% endif %}
</h4>
</div>
<div class="card-body">
<form method="post">
{% csrf_token %}
<!-- Блок 1: Название -->
<div class="mb-4">
<label for="id_name" class="form-label fw-bold fs-5">
{{ form.name.label }}
</label>
{{ form.name }}
{% if form.name.errors %}
<div class="text-danger mt-1">
{{ form.name.errors }}
</div>
{% endif %}
{% if form.name.help_text %}
<div class="form-text">{{ form.name.help_text }}</div>
{% endif %}
</div>
<!-- Блок 2: Slug -->
<div class="mb-4">
<label for="id_slug" class="form-label">
{{ form.slug.label }}
</label>
{{ form.slug }}
{% if form.slug.errors %}
<div class="text-danger mt-1">
{{ form.slug.errors }}
</div>
{% endif %}
{% if form.slug.help_text %}
<div class="form-text">{{ form.slug.help_text }}</div>
{% endif %}
</div>
<!-- Блок 3: Активность -->
<div class="mb-4">
<div class="form-check form-switch">
{{ form.is_active }}
<label class="form-check-label" for="id_is_active">
{{ form.is_active.label }}
</label>
</div>
{% if form.is_active.errors %}
<div class="text-danger mt-1">
{{ form.is_active.errors }}
</div>
{% endif %}
</div>
<!-- Кнопки действий -->
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary">
<i class="bi bi-check-circle"></i>
{% if object %}Сохранить изменения{% else %}Создать тег{% endif %}
</button>
<a href="{% url 'products:tag-list' %}" class="btn btn-outline-secondary">
<i class="bi bi-x-circle"></i> Отмена
</a>
</div>
</form>
</div>
</div>
<!-- Дополнительная информация при редактировании -->
{% if object %}
<div class="card mt-3">
<div class="card-body">
<h6 class="card-title">Дополнительная информация</h6>
<ul class="list-unstyled mb-0">
<li><strong>Создан:</strong> {{ object.created_at|date:"d.m.Y H:i" }}</li>
<li><strong>Обновлен:</strong> {{ object.updated_at|date:"d.m.Y H:i" }}</li>
</ul>
</div>
</div>
{% endif %}
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,147 @@
{% extends 'base.html' %}
{% block title %}Теги товаров{% endblock %}
{% block content %}
<div class="container mt-5">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2>Теги товаров</h2>
<a href="{% url 'products:tag-create' %}" class="btn btn-primary">
<i class="bi bi-plus-circle"></i> Создать тег
</a>
</div>
<!-- Панель поиска и фильтров -->
<div class="card mb-4">
<div class="card-body">
<form method="get" class="row g-3">
<div class="col-md-6">
<label for="search" class="form-label">Поиск</label>
<input type="text" class="form-control" id="search" name="search"
value="{{ search_query }}" placeholder="Поиск по названию или slug...">
</div>
<div class="col-md-4">
<label for="is_active" class="form-label">Статус</label>
<select class="form-select" id="is_active" name="is_active">
<option value="">Все</option>
<option value="1" {% if is_active_filter == '1' %}selected{% endif %}>Активные</option>
<option value="0" {% if is_active_filter == '0' %}selected{% endif %}>Неактивные</option>
</select>
</div>
<div class="col-md-2 d-flex align-items-end">
<button type="submit" class="btn btn-outline-secondary w-100">
<i class="bi bi-search"></i> Найти
</button>
</div>
</form>
</div>
</div>
<!-- Таблица с тегами -->
<div class="card">
<div class="card-body">
{% if tags %}
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead class="table-dark">
<tr>
<th>Название</th>
<th>Slug</th>
<th>Товары</th>
<th>Комплекты</th>
<th>Статус</th>
<th>Действия</th>
</tr>
</thead>
<tbody>
{% for tag in tags %}
<tr>
<td>
<a href="{% url 'products:tag-detail' tag.pk %}" class="text-decoration-none fw-semibold">
{{ tag.name }}
</a>
</td>
<td><code>{{ tag.slug }}</code></td>
<td>
<span class="badge bg-info">{{ tag.products_count }}</span>
</td>
<td>
<span class="badge bg-info">{{ tag.kits_count }}</span>
</td>
<td>
{% if tag.is_active %}
<span class="badge bg-success">Активен</span>
{% else %}
<span class="badge bg-secondary">Неактивен</span>
{% endif %}
</td>
<td>
<div class="btn-group btn-group-sm" role="group">
<a href="{% url 'products:tag-detail' tag.pk %}"
class="btn btn-outline-primary" title="Просмотр">
<i class="bi bi-eye"></i>
</a>
<a href="{% url 'products:tag-update' tag.pk %}"
class="btn btn-outline-warning" title="Изменить">
<i class="bi bi-pencil"></i>
</a>
<a href="{% url 'products:tag-delete' tag.pk %}"
class="btn btn-outline-danger" title="Удалить">
<i class="bi bi-trash"></i>
</a>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<!-- Пагинация -->
{% if is_paginated %}
<nav aria-label="Pagination" class="mt-4">
<ul class="pagination justify-content-center">
{% if page_obj.has_previous %}
<li class="page-item">
<a class="page-link" href="?page=1{% if search_query %}&search={{ search_query }}{% endif %}{% if is_active_filter %}&is_active={{ is_active_filter }}{% endif %}">
Первая
</a>
</li>
<li class="page-item">
<a class="page-link" href="?page={{ page_obj.previous_page_number }}{% if search_query %}&search={{ search_query }}{% endif %}{% if is_active_filter %}&is_active={{ is_active_filter }}{% endif %}">
Назад
</a>
</li>
{% endif %}
<li class="page-item active">
<span class="page-link">
Страница {{ page_obj.number }} из {{ page_obj.paginator.num_pages }}
</span>
</li>
{% if page_obj.has_next %}
<li class="page-item">
<a class="page-link" href="?page={{ page_obj.next_page_number }}{% if search_query %}&search={{ search_query }}{% endif %}{% if is_active_filter %}&is_active={{ is_active_filter }}{% endif %}">
Вперед
</a>
</li>
<li class="page-item">
<a class="page-link" href="?page={{ page_obj.paginator.num_pages }}{% if search_query %}&search={{ search_query }}{% endif %}{% if is_active_filter %}&is_active={{ is_active_filter }}{% endif %}">
Последняя
</a>
</li>
{% endif %}
</ul>
</nav>
{% endif %}
{% else %}
<div class="alert alert-info" role="alert">
<i class="bi bi-info-circle"></i> Теги не найдены.
<a href="{% url 'products:tag-create' %}" class="alert-link">Создать первый тег</a>
</div>
{% endif %}
</div>
</div>
</div>
{% endblock %}

View File

@@ -60,4 +60,11 @@ urlpatterns = [
path('categories/photo/<int:pk>/set-main/', views.category_photo_set_main, name='category-photo-set-main'),
path('categories/photo/<int:pk>/move-up/', views.category_photo_move_up, name='category-photo-move-up'),
path('categories/photo/<int:pk>/move-down/', views.category_photo_move_down, name='category-photo-move-down'),
# CRUD URLs for ProductTag
path('tags/', views.ProductTagListView.as_view(), name='tag-list'),
path('tags/create/', views.ProductTagCreateView.as_view(), name='tag-create'),
path('tags/<int:pk>/', views.ProductTagDetailView.as_view(), name='tag-detail'),
path('tags/<int:pk>/update/', views.ProductTagUpdateView.as_view(), name='tag-update'),
path('tags/<int:pk>/delete/', views.ProductTagDeleteView.as_view(), name='tag-delete'),
]

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