Реализован полный 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:
@@ -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 %}
|
||||
175
myproject/products/templates/products/tag_detail.html
Normal file
175
myproject/products/templates/products/tag_detail.html
Normal 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 %}
|
||||
99
myproject/products/templates/products/tag_form.html
Normal file
99
myproject/products/templates/products/tag_form.html
Normal 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 %}
|
||||
147
myproject/products/templates/products/tag_list.html
Normal file
147
myproject/products/templates/products/tag_list.html
Normal 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 %}
|
||||
Reference in New Issue
Block a user