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

@@ -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 %}