Обновили шапку и вывод всехтоваров. Добавили фильтры
This commit is contained in:
242
myproject/products/templates/products/all_products_list.html
Normal file
242
myproject/products/templates/products/all_products_list.html
Normal file
@@ -0,0 +1,242 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}Все товары и комплекты{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mt-5">
|
||||
<h2 class="mb-4">Товары</h2>
|
||||
|
||||
<!-- Панель быстрых фильтров по категориям -->
|
||||
{% include 'components/category_filter_buttons.html' with categories=filters.categories current_category=filters.current.category show_type_filters=True %}
|
||||
|
||||
<!-- Панель фильтрации и действий -->
|
||||
<div class="card shadow-sm mb-4">
|
||||
<div class="card-body">
|
||||
<!-- Кнопки действий -->
|
||||
<div class="d-flex justify-content-between align-items-center mb-3 flex-wrap">
|
||||
<h5 class="card-title mb-0 me-3">
|
||||
<i class="bi bi-funnel-fill"></i> Поиск и фильтры
|
||||
</h5>
|
||||
|
||||
{% if action_buttons %}
|
||||
<div class="btn-toolbar" role="toolbar">
|
||||
{% for button in action_buttons %}
|
||||
<a href="{{ button.url }}" class="btn {{ button.class|default:'btn-primary' }} btn-sm me-2 mb-2 mb-md-0">
|
||||
{% if button.icon %}<i class="bi bi-{{ button.icon }}"></i>{% endif %}
|
||||
{{ button.text }}
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<hr class="my-3">
|
||||
|
||||
<!-- Форма фильтров -->
|
||||
<form method="get" id="filterForm">
|
||||
<div class="row g-3">
|
||||
<!-- Поле поиска -->
|
||||
<div class="col-12 col-md-6">
|
||||
<label for="search" class="form-label">
|
||||
<i class="bi bi-search"></i> Поиск
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="search"
|
||||
name="search"
|
||||
placeholder="Поиск по названию, артикулу..."
|
||||
value="{{ filters.current.search|default:'' }}"
|
||||
>
|
||||
</div>
|
||||
|
||||
<!-- Фильтр по статусу -->
|
||||
<div class="col-12 col-md-3">
|
||||
<label for="is_active" class="form-label">
|
||||
<i class="bi bi-toggle-on"></i> Статус
|
||||
</label>
|
||||
<select class="form-select" id="is_active" name="is_active">
|
||||
<option value="">Все</option>
|
||||
<option value="1" {% if filters.current.is_active == '1' %}selected{% endif %}>Активные</option>
|
||||
<option value="0" {% if filters.current.is_active == '0' %}selected{% endif %}>Неактивные</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Сохраняем текущую категорию при поиске -->
|
||||
{% if filters.current.category %}
|
||||
<input type="hidden" name="category" value="{{ filters.current.category }}">
|
||||
{% endif %}
|
||||
|
||||
<!-- Кнопки управления фильтрами -->
|
||||
<div class="col-12 col-md-3">
|
||||
<label class="form-label d-none d-md-block"> </label>
|
||||
<div class="d-flex gap-2">
|
||||
<button type="submit" class="btn btn-primary w-100">
|
||||
<i class="bi bi-check-circle"></i> Применить
|
||||
</button>
|
||||
<a href="{{ request.path }}" class="btn btn-outline-secondary">
|
||||
<i class="bi bi-x-circle"></i> Сброс
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if items %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-hover">
|
||||
<thead class="table-dark">
|
||||
<tr>
|
||||
<th style="width: 60px;">Тип</th>
|
||||
<th style="width: 80px;">Фото</th>
|
||||
<th>Название</th>
|
||||
<th>Артикул</th>
|
||||
<th>Категория</th>
|
||||
<th>Цена продажи</th>
|
||||
<th>Статус</th>
|
||||
<th style="width: 200px;">Действия</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for item in items %}
|
||||
<tr>
|
||||
<td>
|
||||
{% if item.item_type == 'product' %}
|
||||
<span class="badge bg-success" title="Товар поштучно">
|
||||
<i class="bi bi-box"></i>
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="badge bg-info" title="Комплект">
|
||||
<i class="bi bi-box-seam"></i>
|
||||
</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if item.photos.all %}
|
||||
{% with photo=item.photos.first %}
|
||||
<img src="{{ photo.image.url }}" alt="{{ item.name }}" style="max-width: 50px; max-height: 50px;" class="img-thumbnail">
|
||||
{% endwith %}
|
||||
{% else %}
|
||||
<span class="text-muted small">Нет фото</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if item.item_type == 'product' %}
|
||||
<a href="{% url 'products:product-detail' item.pk %}">{{ item.name }}</a>
|
||||
{% else %}
|
||||
<a href="{% url 'products:productkit-detail' item.pk %}">{{ item.name }}</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ item.sku }}</td>
|
||||
<td>
|
||||
{% if item.categories.all %}
|
||||
{% for category in item.categories.all %}
|
||||
<span class="badge bg-secondary">{{ category.name }}</span>{% if not forloop.last %} {% endif %}
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<span class="text-muted">-</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if item.item_type == 'product' %}
|
||||
{{ item.sale_price|floatformat:2 }} руб.
|
||||
{% else %}
|
||||
{{ item.get_sale_price|floatformat:2 }} руб.
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if item.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">
|
||||
{% if item.item_type == 'product' %}
|
||||
<a href="{% url 'products:product-detail' item.pk %}" class="btn btn-outline-info" title="Просмотр">
|
||||
<i class="bi bi-eye"></i>
|
||||
</a>
|
||||
{% if perms.products.change_product %}
|
||||
<a href="{% url 'products:product-update' item.pk %}" class="btn btn-outline-primary" title="Изменить">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if perms.products.delete_product %}
|
||||
<a href="{% url 'products:product-delete' item.pk %}" class="btn btn-outline-danger" title="Удалить">
|
||||
<i class="bi bi-trash"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<a href="{% url 'products:productkit-detail' item.pk %}" class="btn btn-outline-info" title="Просмотр">
|
||||
<i class="bi bi-eye"></i>
|
||||
</a>
|
||||
{% if perms.products.change_productkit %}
|
||||
<a href="{% url 'products:productkit-update' item.pk %}" class="btn btn-outline-primary" title="Изменить">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if perms.products.delete_productkit %}
|
||||
<a href="{% url 'products:productkit-delete' item.pk %}" class="btn btn-outline-danger" title="Удалить">
|
||||
<i class="bi bi-trash"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
{% if is_paginated %}
|
||||
<nav aria-label="Page navigation">
|
||||
<ul class="pagination justify-content-center">
|
||||
{% if page_obj.has_previous %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page=1{% for key, value in request.GET.items %}{% if key != 'page' %}&{{ key }}={{ value }}{% endif %}{% endfor %}">Первая</a>
|
||||
</li>
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ page_obj.previous_page_number }}{% for key, value in request.GET.items %}{% if key != 'page' %}&{{ key }}={{ value }}{% endif %}{% endfor %}">Предыдущая</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 }}{% for key, value in request.GET.items %}{% if key != 'page' %}&{{ key }}={{ value }}{% endif %}{% endfor %}">Следующая</a>
|
||||
</li>
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ page_obj.paginator.num_pages }}{% for key, value in request.GET.items %}{% if key != 'page' %}&{{ key }}={{ value }}{% endif %}{% endfor %}">Последняя</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<div class="alert alert-info">
|
||||
<h4><i class="bi bi-info-circle"></i> Товары не найдены</h4>
|
||||
<p>В данный момент нет товаров или комплектов, соответствующих выбранным фильтрам.</p>
|
||||
<div class="mt-3">
|
||||
{% if perms.products.add_product %}
|
||||
<a href="{% url 'products:product-create' %}" class="btn btn-primary me-2">
|
||||
<i class="bi bi-plus-circle"></i> Создать товар
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if perms.products.add_productkit %}
|
||||
<a href="{% url 'products:productkit-create' %}" class="btn btn-outline-primary">
|
||||
<i class="bi bi-plus-circle"></i> Создать комплект
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -4,8 +4,11 @@ from . import views
|
||||
app_name = 'products'
|
||||
|
||||
urlpatterns = [
|
||||
# Combined view for products and kits
|
||||
path('', views.CombinedProductListView.as_view(), name='all-products'),
|
||||
|
||||
# CRUD URLs for Product
|
||||
path('', views.ProductListView.as_view(), name='product-list'),
|
||||
path('products/', views.ProductListView.as_view(), name='product-list'),
|
||||
path('create/', views.ProductCreateView.as_view(), name='product-create'),
|
||||
path('<int:pk>/', views.ProductDetailView.as_view(), name='product-detail'),
|
||||
path('<int:pk>/update/', views.ProductUpdateView.as_view(), name='product-update'),
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
133
myproject/products/views/README.md
Normal file
133
myproject/products/views/README.md
Normal file
@@ -0,0 +1,133 @@
|
||||
# Структура модуля views
|
||||
|
||||
Файл `products/views.py` был разбит на несколько специализированных модулей для улучшения читаемости и поддерживаемости кода.
|
||||
|
||||
## Статистика оптимизации
|
||||
|
||||
### До рефакторинга:
|
||||
- **Файлов:** 1 (`views.py`)
|
||||
- **Строк кода:** 1202
|
||||
- **Дублированный код:** ~400 строк (12 функций управления фото + 3 функции обработки фото)
|
||||
|
||||
### После рефакторинга:
|
||||
- **Файлов:** 7 модулей
|
||||
- **Строк кода:** 1284 (включая документацию)
|
||||
- **Дублированный код:** УСТРАНЁН
|
||||
- **Экономия:** ~400 строк дублированного кода заменены на универсальные функции
|
||||
|
||||
## Структура модулей
|
||||
|
||||
### 1. `__init__.py` (112 строк)
|
||||
Экспортирует все представления для обратной совместимости с `urls.py`.
|
||||
Позволяет использовать импорты вида: `from products.views import ProductListView`
|
||||
|
||||
### 2. `utils.py` (73 строки)
|
||||
Утилиты для работы с фотографиями:
|
||||
- `validate_photo()` - валидация загружаемого фото
|
||||
- `handle_photos()` - **УНИВЕРСАЛЬНАЯ** функция обработки фото (заменяет 3 дублирующиеся функции)
|
||||
|
||||
### 3. `photo_management.py` (310 строк)
|
||||
Универсальные функции управления фотографиями:
|
||||
- **4 базовые функции:** `generic_photo_delete()`, `generic_photo_set_main()`, `generic_photo_move_up()`, `generic_photo_move_down()`
|
||||
- **12 оберток** для Product, ProductKit и Category (заменяют 12 дублирующихся функций из оригинала)
|
||||
|
||||
**Устранённое дублирование:**
|
||||
- Было: 12 отдельных функций (по 4 для каждой модели)
|
||||
- Стало: 4 универсальные функции + 12 простых оберток
|
||||
|
||||
### 4. `product_views.py` (182 строки)
|
||||
CRUD представления для товаров (Product):
|
||||
- `ProductListView` - список товаров с фильтрацией и поиском
|
||||
- `ProductCreateView` - создание товара
|
||||
- `ProductDetailView` - просмотр товара
|
||||
- `ProductUpdateView` - редактирование товара
|
||||
- `ProductDeleteView` - удаление товара
|
||||
|
||||
### 5. `productkit_views.py` (249 строк)
|
||||
CRUD представления для комплектов (ProductKit):
|
||||
- `ProductKitListView` - список комплектов
|
||||
- `ProductKitCreateView` - создание комплекта с компонентами
|
||||
- `ProductKitDetailView` - просмотр комплекта
|
||||
- `ProductKitUpdateView` - редактирование комплекта
|
||||
- `ProductKitDeleteView` - удаление комплекта
|
||||
|
||||
### 6. `category_views.py` (280 строк)
|
||||
CRUD представления для категорий (ProductCategory):
|
||||
- `TreeItem` - класс для элемента дерева категорий
|
||||
- `ProductCategoryListView` - иерархическое дерево категорий с товарами и комплектами
|
||||
- `ProductCategoryCreateView` - создание категории
|
||||
- `ProductCategoryDetailView` - просмотр категории
|
||||
- `ProductCategoryUpdateView` - редактирование категории
|
||||
- `ProductCategoryDeleteView` - удаление категории
|
||||
|
||||
### 7. `api_views.py` (78 строк)
|
||||
API представления:
|
||||
- `search_products_and_variants()` - поиск товаров и групп вариантов для автокомплита
|
||||
|
||||
## Преимущества новой структуры
|
||||
|
||||
### ✅ Устранено дублирование
|
||||
- 12 функций управления фото → 4 универсальные + 12 простых оберток
|
||||
- 3 функции обработки фото → 1 универсальная функция
|
||||
|
||||
### ✅ Улучшена организация
|
||||
- Логическое разделение по функциональным областям
|
||||
- Каждый модуль отвечает за свою сущность (Product, ProductKit, Category)
|
||||
- Легко найти нужный код
|
||||
|
||||
### ✅ Упрощена поддержка
|
||||
- Изменения в одном типе представлений не затрагивают другие
|
||||
- Проще тестировать отдельные компоненты
|
||||
- Легче добавлять новый функционал
|
||||
|
||||
### ✅ Обратная совместимость
|
||||
- Все импорты в `urls.py` работают без изменений
|
||||
- Благодаря `__init__.py` внешний API не изменился
|
||||
|
||||
### ✅ Следование принципам
|
||||
- **SRP (Single Responsibility Principle)** - каждый модуль отвечает за одну область
|
||||
- **DRY (Don't Repeat Yourself)** - устранено дублирование кода
|
||||
- **Separation of Concerns** - разделение по ответственности
|
||||
|
||||
## Примеры использования
|
||||
|
||||
### Импорт представлений (работает как раньше):
|
||||
```python
|
||||
from products.views import ProductListView, ProductCreateView
|
||||
from products.views import productkit_photo_delete
|
||||
from products.views import search_products_and_variants
|
||||
```
|
||||
|
||||
### Импорт из конкретного модуля (новая возможность):
|
||||
```python
|
||||
from products.views.product_views import ProductListView
|
||||
from products.views.photo_management import generic_photo_delete
|
||||
from products.views.utils import validate_photo
|
||||
```
|
||||
|
||||
## Изменения в коде
|
||||
|
||||
### Удалено:
|
||||
- Неиспользуемый импорт `import json`
|
||||
- Комментарий-мусор "Временный файл для добавления в views.py"
|
||||
|
||||
### Добавлено:
|
||||
- Docstrings для всех модулей
|
||||
- Комментарии к универсальным функциям
|
||||
- Документация параметров функций
|
||||
|
||||
## Тестирование
|
||||
|
||||
После рефакторинга рекомендуется:
|
||||
1. Запустить Django сервер: `python manage.py runserver`
|
||||
2. Проверить все CRUD операции для Product, ProductKit и Category
|
||||
3. Проверить управление фотографиями (upload, delete, set main, move up/down)
|
||||
4. Проверить API endpoint для поиска товаров
|
||||
|
||||
## Потенциальные улучшения
|
||||
|
||||
В будущем можно:
|
||||
1. Добавить базовый класс `BasePhotoView` для дальнейшего упрощения
|
||||
2. Вынести общую логику ListView в миксины
|
||||
3. Добавить unit-тесты для каждого модуля
|
||||
4. Создать отдельный модуль для миксинов и базовых классов
|
||||
114
myproject/products/views/__init__.py
Normal file
114
myproject/products/views/__init__.py
Normal file
@@ -0,0 +1,114 @@
|
||||
"""
|
||||
Экспорт всех представлений для обратной совместимости с urls.py.
|
||||
Позволяет использовать импорты вида: from products.views import ProductListView
|
||||
"""
|
||||
|
||||
# Утилиты
|
||||
from .utils import validate_photo, handle_photos
|
||||
|
||||
# Управление фотографиями (Product)
|
||||
from .photo_management import (
|
||||
product_photo_delete,
|
||||
product_photo_set_main,
|
||||
product_photo_move_up,
|
||||
product_photo_move_down,
|
||||
)
|
||||
|
||||
# Управление фотографиями (ProductKit)
|
||||
from .photo_management import (
|
||||
productkit_photo_delete,
|
||||
productkit_photo_set_main,
|
||||
productkit_photo_move_up,
|
||||
productkit_photo_move_down,
|
||||
)
|
||||
|
||||
# Управление фотографиями (Category)
|
||||
from .photo_management import (
|
||||
category_photo_delete,
|
||||
category_photo_set_main,
|
||||
category_photo_move_up,
|
||||
category_photo_move_down,
|
||||
)
|
||||
|
||||
# CRUD представления для Product
|
||||
from .product_views import (
|
||||
ProductListView,
|
||||
ProductCreateView,
|
||||
ProductDetailView,
|
||||
ProductUpdateView,
|
||||
ProductDeleteView,
|
||||
CombinedProductListView,
|
||||
)
|
||||
|
||||
# CRUD представления для ProductKit
|
||||
from .productkit_views import (
|
||||
ProductKitListView,
|
||||
ProductKitCreateView,
|
||||
ProductKitDetailView,
|
||||
ProductKitUpdateView,
|
||||
ProductKitDeleteView,
|
||||
)
|
||||
|
||||
# CRUD представления для Category
|
||||
from .category_views import (
|
||||
TreeItem,
|
||||
ProductCategoryListView,
|
||||
ProductCategoryCreateView,
|
||||
ProductCategoryDetailView,
|
||||
ProductCategoryUpdateView,
|
||||
ProductCategoryDeleteView,
|
||||
)
|
||||
|
||||
# API представления
|
||||
from .api_views import search_products_and_variants
|
||||
|
||||
|
||||
__all__ = [
|
||||
# Утилиты
|
||||
'validate_photo',
|
||||
'handle_photos',
|
||||
|
||||
# Управление фотографиями Product
|
||||
'product_photo_delete',
|
||||
'product_photo_set_main',
|
||||
'product_photo_move_up',
|
||||
'product_photo_move_down',
|
||||
|
||||
# Управление фотографиями ProductKit
|
||||
'productkit_photo_delete',
|
||||
'productkit_photo_set_main',
|
||||
'productkit_photo_move_up',
|
||||
'productkit_photo_move_down',
|
||||
|
||||
# Управление фотографиями Category
|
||||
'category_photo_delete',
|
||||
'category_photo_set_main',
|
||||
'category_photo_move_up',
|
||||
'category_photo_move_down',
|
||||
|
||||
# Product CRUD
|
||||
'ProductListView',
|
||||
'ProductCreateView',
|
||||
'ProductDetailView',
|
||||
'ProductUpdateView',
|
||||
'ProductDeleteView',
|
||||
'CombinedProductListView',
|
||||
|
||||
# ProductKit CRUD
|
||||
'ProductKitListView',
|
||||
'ProductKitCreateView',
|
||||
'ProductKitDetailView',
|
||||
'ProductKitUpdateView',
|
||||
'ProductKitDeleteView',
|
||||
|
||||
# Category CRUD
|
||||
'TreeItem',
|
||||
'ProductCategoryListView',
|
||||
'ProductCategoryCreateView',
|
||||
'ProductCategoryDetailView',
|
||||
'ProductCategoryUpdateView',
|
||||
'ProductCategoryDeleteView',
|
||||
|
||||
# API
|
||||
'search_products_and_variants',
|
||||
]
|
||||
78
myproject/products/views/api_views.py
Normal file
78
myproject/products/views/api_views.py
Normal file
@@ -0,0 +1,78 @@
|
||||
"""
|
||||
API представления для приложения products.
|
||||
"""
|
||||
from django.http import JsonResponse
|
||||
from django.db import models
|
||||
|
||||
from ..models import Product, ProductVariantGroup
|
||||
|
||||
|
||||
def search_products_and_variants(request):
|
||||
"""
|
||||
API endpoint для поиска товаров и групп вариантов.
|
||||
Используется для автокомплита при добавлении компонентов в комплект.
|
||||
|
||||
Параметры GET:
|
||||
- q: строка поиска
|
||||
- type: 'product' или 'variant' (опционально, если не указано - поиск по обоим)
|
||||
|
||||
Возвращает JSON список:
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Роза красная Freedom 50см",
|
||||
"sku": "PROD-000001",
|
||||
"type": "product",
|
||||
"price": "150.00"
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Роза красная Freedom",
|
||||
"type": "variant",
|
||||
"count": 3
|
||||
}
|
||||
]
|
||||
"""
|
||||
query = request.GET.get('q', '').strip()
|
||||
search_type = request.GET.get('type', 'all')
|
||||
|
||||
if not query or len(query) < 2:
|
||||
return JsonResponse({'results': []})
|
||||
|
||||
results = []
|
||||
|
||||
# Поиск товаров
|
||||
if search_type in ['all', 'product']:
|
||||
products = Product.objects.filter(
|
||||
models.Q(name__icontains=query) |
|
||||
models.Q(sku__icontains=query) |
|
||||
models.Q(description__icontains=query),
|
||||
is_active=True
|
||||
).values('id', 'name', 'sku', 'sale_price')[:10]
|
||||
|
||||
for product in products:
|
||||
results.append({
|
||||
'id': product['id'],
|
||||
'name': f"{product['name']} ({product['sku']})",
|
||||
'sku': product['sku'],
|
||||
'type': 'product',
|
||||
'price': str(product['sale_price'])
|
||||
})
|
||||
|
||||
# Поиск групп вариантов
|
||||
if search_type in ['all', 'variant']:
|
||||
variants = ProductVariantGroup.objects.filter(
|
||||
models.Q(name__icontains=query) |
|
||||
models.Q(description__icontains=query)
|
||||
).prefetch_related('products')[:10]
|
||||
|
||||
for variant in variants:
|
||||
count = variant.products.filter(is_active=True).count()
|
||||
results.append({
|
||||
'id': variant.id,
|
||||
'name': f"{variant.name} ({count} вариантов)",
|
||||
'type': 'variant',
|
||||
'count': count
|
||||
})
|
||||
|
||||
return JsonResponse({'results': results})
|
||||
280
myproject/products/views/category_views.py
Normal file
280
myproject/products/views/category_views.py
Normal file
@@ -0,0 +1,280 @@
|
||||
"""
|
||||
CRUD представления для категорий товаров (ProductCategory).
|
||||
Включает иерархическое дерево категорий с товарами и комплектами.
|
||||
"""
|
||||
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, reverse
|
||||
from django.shortcuts import redirect
|
||||
from django.db.models import Q
|
||||
|
||||
from ..models import ProductCategory, ProductCategoryPhoto
|
||||
from ..forms import ProductCategoryForm
|
||||
from .utils import handle_photos
|
||||
|
||||
|
||||
class TreeItem:
|
||||
"""
|
||||
Универсальный элемент дерева категорий.
|
||||
Может представлять категорию, товар или набор.
|
||||
"""
|
||||
def __init__(self, obj, item_type, depth, parent_category_id=None):
|
||||
self.obj = obj
|
||||
self.item_type = item_type # 'category', 'product', 'kit'
|
||||
self.depth = depth
|
||||
self.pk = obj.pk
|
||||
self.name = obj.name
|
||||
self.sku = getattr(obj, 'sku', None)
|
||||
|
||||
if item_type == 'category':
|
||||
self.has_children = obj.children.exists()
|
||||
self.parent_id = obj.parent.pk if obj.parent else None
|
||||
else:
|
||||
# Товары и наборы не имеют детей
|
||||
self.has_children = False
|
||||
# Виртуальный parent_id = ID категории (для JavaScript)
|
||||
self.parent_id = parent_category_id
|
||||
|
||||
# Получение цены
|
||||
if item_type == 'product':
|
||||
self.price = obj.sale_price
|
||||
elif item_type == 'kit':
|
||||
self.price = obj.get_sale_price()
|
||||
else:
|
||||
self.price = None
|
||||
|
||||
|
||||
class ProductCategoryListView(LoginRequiredMixin, ListView):
|
||||
model = ProductCategory
|
||||
template_name = 'products/category_list.html'
|
||||
context_object_name = 'categories'
|
||||
paginate_by = None # Отключаем пагинацию для иерархического отображения
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = super().get_queryset()
|
||||
# Добавляем prefetch_related для оптимизации запросов к фото
|
||||
queryset = queryset.prefetch_related('photos', 'children')
|
||||
|
||||
# Поиск по названию и артикулу
|
||||
search_query = self.request.GET.get('search')
|
||||
if search_query:
|
||||
queryset = queryset.filter(
|
||||
Q(name__icontains=search_query) |
|
||||
Q(sku__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
|
||||
|
||||
def build_category_tree(self, queryset):
|
||||
"""
|
||||
Строит иерархическое дерево категорий с товарами и наборами.
|
||||
Возвращает плоский список TreeItem объектов.
|
||||
"""
|
||||
# Получаем все категории из queryset с prefetch для товаров и наборов
|
||||
all_categories = list(queryset.select_related('parent')
|
||||
.prefetch_related('photos', 'children',
|
||||
'products', 'kits'))
|
||||
|
||||
# Создаем словарь для быстрого доступа по ID
|
||||
categories_dict = {cat.pk: cat for cat in all_categories}
|
||||
|
||||
# Добавляем служебные поля
|
||||
for cat in all_categories:
|
||||
cat.depth = 0
|
||||
cat.has_children = cat.children.exists()
|
||||
|
||||
# Находим корневые категории (parent=None или parent не в нашем queryset)
|
||||
queryset_ids = set(categories_dict.keys())
|
||||
root_categories = [
|
||||
cat for cat in all_categories
|
||||
if cat.parent is None or cat.parent.pk not in queryset_ids
|
||||
]
|
||||
|
||||
# Сортируем корневые по имени
|
||||
root_categories.sort(key=lambda x: x.name.lower())
|
||||
|
||||
# Рекурсивно строим дерево
|
||||
result = []
|
||||
|
||||
def add_category_and_children(category, depth=0):
|
||||
# 1. Добавляем категорию как TreeItem
|
||||
tree_item = TreeItem(category, 'category', depth)
|
||||
result.append(tree_item)
|
||||
|
||||
# 2. Добавляем активные товары этой категории (отсортированные по имени)
|
||||
products = category.products.filter(is_active=True).order_by('name')
|
||||
for product in products:
|
||||
product_item = TreeItem(product, 'product', depth + 1, category.pk)
|
||||
result.append(product_item)
|
||||
|
||||
# 3. Добавляем активные наборы этой категории (отсортированные по имени)
|
||||
kits = category.kits.filter(is_active=True).order_by('name')
|
||||
for kit in kits:
|
||||
kit_item = TreeItem(kit, 'kit', depth + 1, category.pk)
|
||||
result.append(kit_item)
|
||||
|
||||
# 4. Рекурсивно обрабатываем дочерние категории
|
||||
children = [
|
||||
cat for cat in all_categories
|
||||
if cat.parent and cat.parent.pk == category.pk
|
||||
]
|
||||
# Сортируем детей по имени
|
||||
children.sort(key=lambda x: x.name.lower())
|
||||
|
||||
# Рекурсивно добавляем детей
|
||||
for child in children:
|
||||
add_category_and_children(child, depth + 1)
|
||||
|
||||
# Обходим все корневые категории
|
||||
for root in root_categories:
|
||||
add_category_and_children(root, 0)
|
||||
|
||||
return result
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
||||
# Строим иерархическое дерево
|
||||
queryset = self.get_queryset()
|
||||
category_tree = self.build_category_tree(queryset)
|
||||
context['category_tree'] = category_tree
|
||||
|
||||
# Флаг для автоматического раскрытия при поиске
|
||||
context['has_search'] = bool(self.request.GET.get('search'))
|
||||
|
||||
# Фильтры для панели фильтрации
|
||||
context['filters'] = {
|
||||
'search': self.request.GET.get('search', ''),
|
||||
'is_active': self.request.GET.get('is_active', ''),
|
||||
'current': {
|
||||
'search': self.request.GET.get('search', ''),
|
||||
'is_active': self.request.GET.get('is_active', ''),
|
||||
}
|
||||
}
|
||||
# Кнопки действий
|
||||
context['action_buttons'] = [
|
||||
{'url': reverse('products:category-create'), 'text': 'Создать категорию', 'icon': 'plus-circle'}
|
||||
]
|
||||
return context
|
||||
|
||||
|
||||
class ProductCategoryCreateView(LoginRequiredMixin, CreateView):
|
||||
model = ProductCategory
|
||||
form_class = ProductCategoryForm
|
||||
template_name = 'products/category_form.html'
|
||||
success_url = reverse_lazy('products:category-list')
|
||||
|
||||
def form_valid(self, form):
|
||||
# Сохраняем категорию
|
||||
self.object = form.save()
|
||||
messages.success(self.request, f'Категория "{self.object.name}" создана успешно.')
|
||||
|
||||
# Обработка загрузки фотографий
|
||||
errors = handle_photos(self.request, self.object, ProductCategoryPhoto, 'category')
|
||||
if errors:
|
||||
for error in errors:
|
||||
messages.warning(self.request, error)
|
||||
|
||||
return redirect(self.get_success_url())
|
||||
|
||||
def form_invalid(self, form):
|
||||
messages.error(self.request, 'Пожалуйста, исправьте ошибки в форме.')
|
||||
return super().form_invalid(form)
|
||||
|
||||
|
||||
class ProductCategoryDetailView(LoginRequiredMixin, DetailView):
|
||||
model = ProductCategory
|
||||
template_name = 'products/category_detail.html'
|
||||
context_object_name = 'category'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
# Получаем фотографии категории
|
||||
context['category_photos'] = self.object.photos.all().order_by('order')
|
||||
context['photos_count'] = context['category_photos'].count()
|
||||
# Получаем дочерние категории
|
||||
context['children_categories'] = self.object.children.filter(is_active=True)
|
||||
# Получаем товары в категории
|
||||
context['products'] = self.object.products.filter(is_active=True)[:20]
|
||||
context['products_count'] = self.object.products.filter(is_active=True).count()
|
||||
return context
|
||||
|
||||
|
||||
class ProductCategoryUpdateView(LoginRequiredMixin, UpdateView):
|
||||
model = ProductCategory
|
||||
form_class = ProductCategoryForm
|
||||
template_name = 'products/category_form.html'
|
||||
success_url = reverse_lazy('products:category-list')
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
# Добавляем фотографии для отображения в форме
|
||||
context['category_photos'] = self.object.photos.all().order_by('order')
|
||||
context['photos_count'] = context['category_photos'].count()
|
||||
return context
|
||||
|
||||
def form_valid(self, form):
|
||||
# Сохраняем категорию
|
||||
self.object = form.save()
|
||||
messages.success(self.request, f'Категория "{self.object.name}" обновлена успешно.')
|
||||
|
||||
# Обработка загрузки новых фотографий
|
||||
errors = handle_photos(self.request, self.object, ProductCategoryPhoto, 'category')
|
||||
if errors:
|
||||
for error in errors:
|
||||
messages.warning(self.request, error)
|
||||
|
||||
return redirect(self.get_success_url())
|
||||
|
||||
def form_invalid(self, form):
|
||||
messages.error(self.request, 'Пожалуйста, исправьте ошибки в форме.')
|
||||
return super().form_invalid(form)
|
||||
|
||||
|
||||
class ProductCategoryDeleteView(LoginRequiredMixin, DeleteView):
|
||||
model = ProductCategory
|
||||
template_name = 'products/category_confirm_delete.html'
|
||||
success_url = reverse_lazy('products:category-list')
|
||||
context_object_name = 'category'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
# Проверяем, есть ли товары в категории
|
||||
context['products_count'] = self.object.products.count()
|
||||
context['children_count'] = self.object.children.count()
|
||||
return context
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
self.object = self.get_object()
|
||||
|
||||
# Проверяем, есть ли товары в категории
|
||||
if self.object.products.exists():
|
||||
messages.error(
|
||||
request,
|
||||
f'Невозможно удалить категорию "{self.object.name}", '
|
||||
f'так как в ней есть товары ({self.object.products.count()} шт.). '
|
||||
f'Сначала удалите или переместите товары.'
|
||||
)
|
||||
return redirect('products:category-detail', pk=self.object.pk)
|
||||
|
||||
# Проверяем, есть ли дочерние категории
|
||||
if self.object.children.exists():
|
||||
messages.error(
|
||||
request,
|
||||
f'Невозможно удалить категорию "{self.object.name}", '
|
||||
f'так как у неё есть подкатегории ({self.object.children.count()} шт.). '
|
||||
f'Сначала удалите или переместите подкатегории.'
|
||||
)
|
||||
return redirect('products:category-detail', pk=self.object.pk)
|
||||
|
||||
messages.success(request, f'Категория "{self.object.name}" удалена успешно.')
|
||||
return super().post(request, *args, **kwargs)
|
||||
310
myproject/products/views/photo_management.py
Normal file
310
myproject/products/views/photo_management.py
Normal file
@@ -0,0 +1,310 @@
|
||||
"""
|
||||
Универсальные функции для управления фотографиями товаров, комплектов и категорий.
|
||||
Устраняет дублирование кода для операций: delete, set_main, move_up, move_down.
|
||||
"""
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.contrib import messages
|
||||
|
||||
from ..models import ProductPhoto, ProductKitPhoto, ProductCategoryPhoto
|
||||
|
||||
|
||||
# ====================================
|
||||
# Универсальные функции
|
||||
# ====================================
|
||||
|
||||
def generic_photo_delete(request, pk, photo_model, redirect_url_name, parent_attr, permission):
|
||||
"""
|
||||
Универсальное удаление фотографии.
|
||||
|
||||
Args:
|
||||
request: HTTP request
|
||||
pk: ID фотографии
|
||||
photo_model: Модель фото (ProductPhoto, ProductKitPhoto, ProductCategoryPhoto)
|
||||
redirect_url_name: Имя URL для редиректа ('products:product-update', etc.)
|
||||
parent_attr: Имя атрибута родителя ('product', 'kit', 'category')
|
||||
permission: Требуемое разрешение ('products.change_product', etc.)
|
||||
"""
|
||||
photo = get_object_or_404(photo_model, pk=pk)
|
||||
parent = getattr(photo, parent_attr)
|
||||
parent_id = parent.id
|
||||
|
||||
# Проверка прав доступа
|
||||
if not request.user.has_perm(permission):
|
||||
messages.error(request, 'У вас нет прав для удаления фотографий.')
|
||||
return redirect(redirect_url_name, pk=parent_id)
|
||||
|
||||
photo.delete()
|
||||
messages.success(request, 'Фото успешно удалено!')
|
||||
|
||||
return redirect(redirect_url_name, pk=parent_id)
|
||||
|
||||
|
||||
def generic_photo_set_main(request, pk, photo_model, redirect_url_name, parent_attr, permission):
|
||||
"""
|
||||
Универсальная установка фото как главного (order = 0).
|
||||
|
||||
Args:
|
||||
request: HTTP request
|
||||
pk: ID фотографии
|
||||
photo_model: Модель фото
|
||||
redirect_url_name: Имя URL для редиректа
|
||||
parent_attr: Имя атрибута родителя ('product', 'kit', 'category')
|
||||
permission: Требуемое разрешение
|
||||
"""
|
||||
photo = get_object_or_404(photo_model, pk=pk)
|
||||
parent = getattr(photo, parent_attr)
|
||||
parent_id = parent.id
|
||||
|
||||
# Проверка прав доступа
|
||||
if not request.user.has_perm(permission):
|
||||
messages.error(request, 'У вас нет прав для изменения порядка фотографий.')
|
||||
return redirect(redirect_url_name, pk=parent_id)
|
||||
|
||||
# Получаем все фото этого родительского объекта
|
||||
filter_kwargs = {f"{parent_attr}_id": parent_id}
|
||||
photos = photo_model.objects.filter(**filter_kwargs).order_by('order')
|
||||
|
||||
# Если это уже главное фото, ничего не делаем
|
||||
if photo.order == 0:
|
||||
messages.info(request, 'Это фото уже установлено как главное.')
|
||||
return redirect(redirect_url_name, pk=parent_id)
|
||||
|
||||
# Меняем порядок: текущее главное фото становится вторым
|
||||
old_order = photo.order
|
||||
for p in photos:
|
||||
if p.pk == photo.pk:
|
||||
p.order = 0
|
||||
p.save()
|
||||
elif p.order == 0:
|
||||
p.order = old_order
|
||||
p.save()
|
||||
|
||||
messages.success(request, 'Фото установлено как главное!')
|
||||
return redirect(redirect_url_name, pk=parent_id)
|
||||
|
||||
|
||||
def generic_photo_move_up(request, pk, photo_model, redirect_url_name, parent_attr, permission):
|
||||
"""
|
||||
Универсальное перемещение фото вверх (уменьшить order).
|
||||
|
||||
Args:
|
||||
request: HTTP request
|
||||
pk: ID фотографии
|
||||
photo_model: Модель фото
|
||||
redirect_url_name: Имя URL для редиректа
|
||||
parent_attr: Имя атрибута родителя ('product', 'kit', 'category')
|
||||
permission: Требуемое разрешение
|
||||
"""
|
||||
photo = get_object_or_404(photo_model, pk=pk)
|
||||
parent = getattr(photo, parent_attr)
|
||||
parent_id = parent.id
|
||||
|
||||
# Проверка прав доступа
|
||||
if not request.user.has_perm(permission):
|
||||
messages.error(request, 'У вас нет прав для изменения порядка фотографий.')
|
||||
return redirect(redirect_url_name, pk=parent_id)
|
||||
|
||||
# Если это уже первое фото
|
||||
if photo.order == 0:
|
||||
messages.info(request, 'Это фото уже первое в списке.')
|
||||
return redirect(redirect_url_name, pk=parent_id)
|
||||
|
||||
# Находим предыдущее фото
|
||||
filter_kwargs = {
|
||||
f"{parent_attr}_id": parent_id,
|
||||
'order__lt': photo.order
|
||||
}
|
||||
prev_photo = photo_model.objects.filter(**filter_kwargs).order_by('-order').first()
|
||||
|
||||
if prev_photo:
|
||||
# Меняем местами
|
||||
photo.order, prev_photo.order = prev_photo.order, photo.order
|
||||
photo.save()
|
||||
prev_photo.save()
|
||||
messages.success(request, 'Фото перемещено вверх!')
|
||||
|
||||
return redirect(redirect_url_name, pk=parent_id)
|
||||
|
||||
|
||||
def generic_photo_move_down(request, pk, photo_model, redirect_url_name, parent_attr, permission):
|
||||
"""
|
||||
Универсальное перемещение фото вниз (увеличить order).
|
||||
|
||||
Args:
|
||||
request: HTTP request
|
||||
pk: ID фотографии
|
||||
photo_model: Модель фото
|
||||
redirect_url_name: Имя URL для редиректа
|
||||
parent_attr: Имя атрибута родителя ('product', 'kit', 'category')
|
||||
permission: Требуемое разрешение
|
||||
"""
|
||||
photo = get_object_or_404(photo_model, pk=pk)
|
||||
parent = getattr(photo, parent_attr)
|
||||
parent_id = parent.id
|
||||
|
||||
# Проверка прав доступа
|
||||
if not request.user.has_perm(permission):
|
||||
messages.error(request, 'У вас нет прав для изменения порядка фотографий.')
|
||||
return redirect(redirect_url_name, pk=parent_id)
|
||||
|
||||
# Находим следующее фото
|
||||
filter_kwargs = {
|
||||
f"{parent_attr}_id": parent_id,
|
||||
'order__gt': photo.order
|
||||
}
|
||||
next_photo = photo_model.objects.filter(**filter_kwargs).order_by('order').first()
|
||||
|
||||
if next_photo:
|
||||
# Меняем местами
|
||||
photo.order, next_photo.order = next_photo.order, photo.order
|
||||
photo.save()
|
||||
next_photo.save()
|
||||
messages.success(request, 'Фото перемещено вниз!')
|
||||
else:
|
||||
messages.info(request, 'Это фото уже последнее в списке.')
|
||||
|
||||
return redirect(redirect_url_name, pk=parent_id)
|
||||
|
||||
|
||||
# ====================================
|
||||
# Обертки для Product Photos
|
||||
# ====================================
|
||||
|
||||
def product_photo_delete(request, pk):
|
||||
"""Удаление фотографии товара"""
|
||||
return generic_photo_delete(
|
||||
request, pk,
|
||||
photo_model=ProductPhoto,
|
||||
redirect_url_name='products:product-update',
|
||||
parent_attr='product',
|
||||
permission='products.change_product'
|
||||
)
|
||||
|
||||
|
||||
def product_photo_set_main(request, pk):
|
||||
"""Установка фото товара как главного (order = 0)"""
|
||||
return generic_photo_set_main(
|
||||
request, pk,
|
||||
photo_model=ProductPhoto,
|
||||
redirect_url_name='products:product-update',
|
||||
parent_attr='product',
|
||||
permission='products.change_product'
|
||||
)
|
||||
|
||||
|
||||
def product_photo_move_up(request, pk):
|
||||
"""Переместить фото товара вверх (уменьшить order)"""
|
||||
return generic_photo_move_up(
|
||||
request, pk,
|
||||
photo_model=ProductPhoto,
|
||||
redirect_url_name='products:product-update',
|
||||
parent_attr='product',
|
||||
permission='products.change_product'
|
||||
)
|
||||
|
||||
|
||||
def product_photo_move_down(request, pk):
|
||||
"""Переместить фото товара вниз (увеличить order)"""
|
||||
return generic_photo_move_down(
|
||||
request, pk,
|
||||
photo_model=ProductPhoto,
|
||||
redirect_url_name='products:product-update',
|
||||
parent_attr='product',
|
||||
permission='products.change_product'
|
||||
)
|
||||
|
||||
|
||||
# ====================================
|
||||
# Обертки для ProductKit Photos
|
||||
# ====================================
|
||||
|
||||
def productkit_photo_delete(request, pk):
|
||||
"""Удаление фотографии комплекта"""
|
||||
return generic_photo_delete(
|
||||
request, pk,
|
||||
photo_model=ProductKitPhoto,
|
||||
redirect_url_name='products:productkit-update',
|
||||
parent_attr='kit',
|
||||
permission='products.change_productkit'
|
||||
)
|
||||
|
||||
|
||||
def productkit_photo_set_main(request, pk):
|
||||
"""Установка фото комплекта как главного (order = 0)"""
|
||||
return generic_photo_set_main(
|
||||
request, pk,
|
||||
photo_model=ProductKitPhoto,
|
||||
redirect_url_name='products:productkit-update',
|
||||
parent_attr='kit',
|
||||
permission='products.change_productkit'
|
||||
)
|
||||
|
||||
|
||||
def productkit_photo_move_up(request, pk):
|
||||
"""Переместить фото комплекта вверх (уменьшить order)"""
|
||||
return generic_photo_move_up(
|
||||
request, pk,
|
||||
photo_model=ProductKitPhoto,
|
||||
redirect_url_name='products:productkit-update',
|
||||
parent_attr='kit',
|
||||
permission='products.change_productkit'
|
||||
)
|
||||
|
||||
|
||||
def productkit_photo_move_down(request, pk):
|
||||
"""Переместить фото комплекта вниз (увеличить order)"""
|
||||
return generic_photo_move_down(
|
||||
request, pk,
|
||||
photo_model=ProductKitPhoto,
|
||||
redirect_url_name='products:productkit-update',
|
||||
parent_attr='kit',
|
||||
permission='products.change_productkit'
|
||||
)
|
||||
|
||||
|
||||
# ====================================
|
||||
# Обертки для Category Photos
|
||||
# ====================================
|
||||
|
||||
def category_photo_delete(request, pk):
|
||||
"""Удаление фотографии категории"""
|
||||
return generic_photo_delete(
|
||||
request, pk,
|
||||
photo_model=ProductCategoryPhoto,
|
||||
redirect_url_name='products:category-update',
|
||||
parent_attr='category',
|
||||
permission='products.change_productcategory'
|
||||
)
|
||||
|
||||
|
||||
def category_photo_set_main(request, pk):
|
||||
"""Установка фото категории как главного (order = 0)"""
|
||||
return generic_photo_set_main(
|
||||
request, pk,
|
||||
photo_model=ProductCategoryPhoto,
|
||||
redirect_url_name='products:category-update',
|
||||
parent_attr='category',
|
||||
permission='products.change_productcategory'
|
||||
)
|
||||
|
||||
|
||||
def category_photo_move_up(request, pk):
|
||||
"""Переместить фото категории вверх (уменьшить order)"""
|
||||
return generic_photo_move_up(
|
||||
request, pk,
|
||||
photo_model=ProductCategoryPhoto,
|
||||
redirect_url_name='products:category-update',
|
||||
parent_attr='category',
|
||||
permission='products.change_productcategory'
|
||||
)
|
||||
|
||||
|
||||
def category_photo_move_down(request, pk):
|
||||
"""Переместить фото категории вниз (увеличить order)"""
|
||||
return generic_photo_move_down(
|
||||
request, pk,
|
||||
photo_model=ProductCategoryPhoto,
|
||||
redirect_url_name='products:category-update',
|
||||
parent_attr='category',
|
||||
permission='products.change_productcategory'
|
||||
)
|
||||
289
myproject/products/views/product_views.py
Normal file
289
myproject/products/views/product_views.py
Normal file
@@ -0,0 +1,289 @@
|
||||
"""
|
||||
CRUD представления для товаров (Product).
|
||||
"""
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin
|
||||
from django.views.generic import ListView, CreateView, DetailView, UpdateView, DeleteView
|
||||
from django.urls import reverse_lazy
|
||||
from django.db.models import Q
|
||||
from itertools import chain
|
||||
|
||||
from ..models import Product, ProductCategory, ProductTag, ProductKit
|
||||
from ..forms import ProductForm
|
||||
from .utils import handle_photos
|
||||
from ..models import ProductPhoto
|
||||
|
||||
|
||||
class ProductListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
|
||||
model = Product
|
||||
template_name = 'products/product_list.html'
|
||||
context_object_name = 'products'
|
||||
permission_required = 'products.view_product'
|
||||
paginate_by = 10
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = super().get_queryset()
|
||||
# Добавляем prefetch_related для оптимизации запросов к категориям
|
||||
queryset = queryset.prefetch_related('categories', 'photos', 'tags')
|
||||
|
||||
# Улучшенный поиск по нескольким полям
|
||||
search_query = self.request.GET.get('search')
|
||||
if search_query:
|
||||
# Ищем по названию, артикулу, описанию, категориям и ключевым словам
|
||||
queryset = queryset.filter(
|
||||
Q(name__icontains=search_query) |
|
||||
Q(sku__icontains=search_query) |
|
||||
Q(description__icontains=search_query) |
|
||||
Q(categories__name__icontains=search_query) |
|
||||
Q(search_keywords__icontains=search_query)
|
||||
).distinct()
|
||||
|
||||
# Фильтр по категории
|
||||
category_id = self.request.GET.get('category')
|
||||
if category_id:
|
||||
queryset = queryset.filter(categories__id=category_id)
|
||||
|
||||
# Фильтр по статусу
|
||||
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)
|
||||
|
||||
# Фильтр по тегам
|
||||
tags = self.request.GET.getlist('tags')
|
||||
if tags:
|
||||
queryset = queryset.filter(tags__id__in=tags).distinct()
|
||||
|
||||
return queryset.order_by('-created_at')
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
||||
# Данные для фильтров
|
||||
context['filters'] = {
|
||||
'categories': ProductCategory.objects.filter(is_active=True),
|
||||
'tags': ProductTag.objects.all(),
|
||||
'current': {
|
||||
'search': self.request.GET.get('search', ''),
|
||||
'category': self.request.GET.get('category', ''),
|
||||
'is_active': self.request.GET.get('is_active', ''),
|
||||
'tags': self.request.GET.getlist('tags'),
|
||||
}
|
||||
}
|
||||
|
||||
# Кнопки действий
|
||||
action_buttons = []
|
||||
|
||||
if self.request.user.has_perm('products.add_product'):
|
||||
action_buttons.append({
|
||||
'url': reverse_lazy('products:product-create'),
|
||||
'text': 'Создать товар',
|
||||
'class': 'btn-primary',
|
||||
'icon': 'plus-circle'
|
||||
})
|
||||
|
||||
if self.request.user.has_perm('products.add_productkit'):
|
||||
action_buttons.append({
|
||||
'url': reverse_lazy('products:productkit-create'),
|
||||
'text': 'Создать комплект',
|
||||
'class': 'btn-outline-primary',
|
||||
'icon': 'box-seam'
|
||||
})
|
||||
|
||||
action_buttons.append({
|
||||
'url': reverse_lazy('products:productkit-list'),
|
||||
'text': 'К списку комплектов',
|
||||
'class': 'btn-outline-secondary',
|
||||
'icon': 'list'
|
||||
})
|
||||
|
||||
context['action_buttons'] = action_buttons
|
||||
|
||||
return context
|
||||
|
||||
|
||||
class ProductCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView):
|
||||
model = Product
|
||||
form_class = ProductForm
|
||||
template_name = 'products/product_form.html'
|
||||
permission_required = 'products.add_product'
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse_lazy('products:product-list')
|
||||
|
||||
def form_valid(self, form):
|
||||
response = super().form_valid(form)
|
||||
|
||||
# Handle photo uploads
|
||||
photo_errors = handle_photos(self.request, self.object, ProductPhoto, 'product')
|
||||
if photo_errors:
|
||||
for error in photo_errors:
|
||||
messages.error(self.request, error)
|
||||
|
||||
messages.success(self.request, f'Товар "{form.instance.name}" успешно создан!')
|
||||
return response
|
||||
|
||||
|
||||
class ProductDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView):
|
||||
model = Product
|
||||
template_name = 'products/product_detail.html'
|
||||
context_object_name = 'product'
|
||||
permission_required = 'products.view_product'
|
||||
|
||||
def get_queryset(self):
|
||||
# Prefetch photos to avoid N+1 queries
|
||||
return super().get_queryset().prefetch_related('photos')
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
# Добавляем фотографии товара в контекст
|
||||
context['product_photos'] = self.object.photos.all().order_by('order', 'created_at')
|
||||
context['photos_count'] = self.object.photos.count()
|
||||
return context
|
||||
|
||||
|
||||
class ProductUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView):
|
||||
model = Product
|
||||
form_class = ProductForm
|
||||
template_name = 'products/product_form.html'
|
||||
permission_required = 'products.change_product'
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse_lazy('products:product-list')
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
# Добавляем фотографии товара в контекст
|
||||
context['product_photos'] = self.object.photos.all().order_by('order', 'created_at')
|
||||
context['photos_count'] = self.object.photos.count()
|
||||
return context
|
||||
|
||||
def form_valid(self, form):
|
||||
response = super().form_valid(form)
|
||||
|
||||
# Handle photo uploads
|
||||
photo_errors = handle_photos(self.request, self.object, ProductPhoto, 'product')
|
||||
if photo_errors:
|
||||
for error in photo_errors:
|
||||
messages.error(self.request, error)
|
||||
|
||||
messages.success(self.request, f'Товар "{form.instance.name}" успешно обновлен!')
|
||||
return response
|
||||
|
||||
|
||||
class ProductDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteView):
|
||||
model = Product
|
||||
template_name = 'products/product_confirm_delete.html'
|
||||
context_object_name = 'product'
|
||||
permission_required = 'products.delete_product'
|
||||
|
||||
def get_success_url(self):
|
||||
messages.success(self.request, f'Товар "{self.object.name}" успешно удален!')
|
||||
return reverse_lazy('products:product-list')
|
||||
|
||||
|
||||
class CombinedProductListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
|
||||
"""
|
||||
Объединенное представление для товаров и комплектов.
|
||||
Показывает оба типа продуктов в одном списке.
|
||||
"""
|
||||
template_name = 'products/all_products_list.html'
|
||||
context_object_name = 'items'
|
||||
permission_required = 'products.view_product'
|
||||
paginate_by = 20
|
||||
|
||||
def get_queryset(self):
|
||||
# Получаем товары и комплекты
|
||||
products = Product.objects.prefetch_related('categories', 'photos', 'tags')
|
||||
kits = ProductKit.objects.prefetch_related('categories', 'photos')
|
||||
|
||||
# Применяем фильтры
|
||||
search_query = self.request.GET.get('search')
|
||||
category_id = self.request.GET.get('category')
|
||||
is_active = self.request.GET.get('is_active')
|
||||
|
||||
# Фильтрация по поиску
|
||||
if search_query:
|
||||
products = products.filter(
|
||||
Q(name__icontains=search_query) |
|
||||
Q(sku__icontains=search_query) |
|
||||
Q(description__icontains=search_query) |
|
||||
Q(categories__name__icontains=search_query) |
|
||||
Q(search_keywords__icontains=search_query)
|
||||
).distinct()
|
||||
|
||||
kits = kits.filter(
|
||||
Q(name__icontains=search_query) |
|
||||
Q(sku__icontains=search_query) |
|
||||
Q(description__icontains=search_query) |
|
||||
Q(categories__name__icontains=search_query)
|
||||
).distinct()
|
||||
|
||||
# Фильтрация по категории
|
||||
if category_id:
|
||||
products = products.filter(categories__id=category_id)
|
||||
kits = kits.filter(categories__id=category_id)
|
||||
|
||||
# Фильтрация по статусу
|
||||
if is_active == '1':
|
||||
products = products.filter(is_active=True)
|
||||
kits = kits.filter(is_active=True)
|
||||
elif is_active == '0':
|
||||
products = products.filter(is_active=False)
|
||||
kits = kits.filter(is_active=False)
|
||||
|
||||
# Добавляем type для различения в шаблоне
|
||||
products_list = list(products.order_by('-created_at'))
|
||||
for p in products_list:
|
||||
p.item_type = 'product'
|
||||
|
||||
kits_list = list(kits.order_by('-created_at'))
|
||||
for k in kits_list:
|
||||
k.item_type = 'kit'
|
||||
|
||||
# Объединяем и сортируем по дате создания
|
||||
combined = sorted(
|
||||
chain(products_list, kits_list),
|
||||
key=lambda x: x.created_at,
|
||||
reverse=True
|
||||
)
|
||||
|
||||
return combined
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
||||
# Данные для фильтров
|
||||
context['filters'] = {
|
||||
'categories': ProductCategory.objects.filter(is_active=True),
|
||||
'tags': ProductTag.objects.all(),
|
||||
'current': {
|
||||
'search': self.request.GET.get('search', ''),
|
||||
'category': self.request.GET.get('category', ''),
|
||||
'is_active': self.request.GET.get('is_active', ''),
|
||||
}
|
||||
}
|
||||
|
||||
# Кнопки действий
|
||||
action_buttons = []
|
||||
|
||||
if self.request.user.has_perm('products.add_product'):
|
||||
action_buttons.append({
|
||||
'url': reverse_lazy('products:product-create'),
|
||||
'text': 'Создать товар',
|
||||
'class': 'btn-primary',
|
||||
'icon': 'plus-circle'
|
||||
})
|
||||
|
||||
if self.request.user.has_perm('products.add_productkit'):
|
||||
action_buttons.append({
|
||||
'url': reverse_lazy('products:productkit-create'),
|
||||
'text': 'Создать комплект',
|
||||
'class': 'btn-outline-primary',
|
||||
'icon': 'box-seam'
|
||||
})
|
||||
|
||||
context['action_buttons'] = action_buttons
|
||||
|
||||
return context
|
||||
249
myproject/products/views/productkit_views.py
Normal file
249
myproject/products/views/productkit_views.py
Normal file
@@ -0,0 +1,249 @@
|
||||
"""
|
||||
CRUD представления для комплектов товаров (ProductKit).
|
||||
"""
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin
|
||||
from django.views.generic import ListView, CreateView, DetailView, UpdateView, DeleteView
|
||||
from django.urls import reverse_lazy
|
||||
from django.shortcuts import redirect
|
||||
from django.db import transaction
|
||||
|
||||
from ..models import ProductKit, ProductCategory, ProductTag, ProductKitPhoto
|
||||
from ..forms import ProductKitForm, KitItemFormSetCreate, KitItemFormSetUpdate
|
||||
from .utils import handle_photos
|
||||
|
||||
|
||||
class ProductKitListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
|
||||
model = ProductKit
|
||||
template_name = 'products/productkit_list.html'
|
||||
context_object_name = 'kits'
|
||||
permission_required = 'products.view_productkit'
|
||||
paginate_by = 10
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = super().get_queryset()
|
||||
queryset = queryset.prefetch_related('categories', 'photos', 'kit_items', 'tags')
|
||||
|
||||
# Поиск по названию
|
||||
search_query = self.request.GET.get('search')
|
||||
if search_query:
|
||||
queryset = queryset.filter(name__icontains=search_query)
|
||||
|
||||
# Фильтр по категории
|
||||
category_id = self.request.GET.get('category')
|
||||
if category_id:
|
||||
queryset = queryset.filter(categories__id=category_id)
|
||||
|
||||
# Фильтр по статусу
|
||||
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('-created_at')
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
||||
# Данные для фильтров
|
||||
context['filters'] = {
|
||||
'categories': ProductCategory.objects.filter(is_active=True),
|
||||
'tags': ProductTag.objects.all(),
|
||||
'current': {
|
||||
'search': self.request.GET.get('search', ''),
|
||||
'category': self.request.GET.get('category', ''),
|
||||
'is_active': self.request.GET.get('is_active', ''),
|
||||
'tags': self.request.GET.getlist('tags'),
|
||||
}
|
||||
}
|
||||
|
||||
# Кнопки действий
|
||||
action_buttons = []
|
||||
|
||||
if self.request.user.has_perm('products.add_productkit'):
|
||||
action_buttons.append({
|
||||
'url': reverse_lazy('products:productkit-create'),
|
||||
'text': 'Создать комплект',
|
||||
'class': 'btn-primary',
|
||||
'icon': 'plus-circle'
|
||||
})
|
||||
|
||||
action_buttons.append({
|
||||
'url': reverse_lazy('products:product-list'),
|
||||
'text': 'К товарам',
|
||||
'class': 'btn-outline-primary',
|
||||
'icon': 'box'
|
||||
})
|
||||
|
||||
context['action_buttons'] = action_buttons
|
||||
|
||||
return context
|
||||
|
||||
|
||||
class ProductKitCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView):
|
||||
"""
|
||||
View для создания нового комплекта с компонентами.
|
||||
"""
|
||||
model = ProductKit
|
||||
form_class = ProductKitForm
|
||||
template_name = 'products/productkit_form.html'
|
||||
permission_required = 'products.add_productkit'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
||||
if self.request.POST:
|
||||
context['kititem_formset'] = KitItemFormSetCreate(self.request.POST, instance=self.object)
|
||||
else:
|
||||
context['kititem_formset'] = KitItemFormSetCreate(instance=self.object)
|
||||
|
||||
return context
|
||||
|
||||
def form_valid(self, form):
|
||||
# Получаем формсет из POST
|
||||
kititem_formset = KitItemFormSetCreate(self.request.POST, instance=self.object)
|
||||
|
||||
# Проверяем валидность формсета
|
||||
if kititem_formset.is_valid():
|
||||
try:
|
||||
with transaction.atomic():
|
||||
# Сохраняем основную форму
|
||||
self.object = form.save()
|
||||
|
||||
# Сохраняем компоненты
|
||||
kititem_formset.instance = self.object
|
||||
kititem_formset.save()
|
||||
|
||||
# Обработка фотографий
|
||||
handle_photos(self.request, self.object, ProductKitPhoto, 'kit')
|
||||
|
||||
messages.success(self.request, f'Комплект "{self.object.name}" успешно создан!')
|
||||
|
||||
# Проверяем, какую кнопку нажали
|
||||
if self.request.POST.get('action') == 'continue':
|
||||
return redirect('products:productkit-update', pk=self.object.pk)
|
||||
else:
|
||||
return redirect('products:productkit-list')
|
||||
except Exception as e:
|
||||
messages.error(self.request, f'Ошибка при сохранении: {str(e)}')
|
||||
return self.form_invalid(form)
|
||||
else:
|
||||
# Если формсет невалиден, показываем форму с ошибками
|
||||
messages.error(self.request, 'Пожалуйста, исправьте ошибки в компонентах комплекта.')
|
||||
return self.form_invalid(form)
|
||||
|
||||
def form_invalid(self, form):
|
||||
# Получаем формсет для отображения ошибок
|
||||
context = self.get_context_data(form=form)
|
||||
return self.render_to_response(context)
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse_lazy('products:productkit-list')
|
||||
|
||||
|
||||
class ProductKitUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView):
|
||||
"""
|
||||
View для редактирования существующего комплекта.
|
||||
"""
|
||||
model = ProductKit
|
||||
form_class = ProductKitForm
|
||||
template_name = 'products/productkit_form.html'
|
||||
permission_required = 'products.change_productkit'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
||||
if self.request.POST:
|
||||
context['kititem_formset'] = KitItemFormSetUpdate(self.request.POST, instance=self.object)
|
||||
else:
|
||||
context['kititem_formset'] = KitItemFormSetUpdate(instance=self.object)
|
||||
|
||||
context['productkit_photos'] = self.object.photos.all().order_by('order', 'created_at')
|
||||
context['photos_count'] = self.object.photos.count()
|
||||
|
||||
return context
|
||||
|
||||
def form_valid(self, form):
|
||||
# Получаем формсет из POST
|
||||
kititem_formset = KitItemFormSetUpdate(self.request.POST, instance=self.object)
|
||||
|
||||
# Проверяем валидность формсета
|
||||
if kititem_formset.is_valid():
|
||||
try:
|
||||
with transaction.atomic():
|
||||
# Сохраняем основную форму
|
||||
self.object = form.save()
|
||||
|
||||
# Сохраняем компоненты
|
||||
kititem_formset.instance = self.object
|
||||
kititem_formset.save()
|
||||
|
||||
# Обработка фотографий
|
||||
handle_photos(self.request, self.object, ProductKitPhoto, 'kit')
|
||||
|
||||
messages.success(self.request, f'Комплект "{self.object.name}" успешно обновлен!')
|
||||
|
||||
# Проверяем, какую кнопку нажали
|
||||
if self.request.POST.get('action') == 'continue':
|
||||
return redirect('products:productkit-update', pk=self.object.pk)
|
||||
else:
|
||||
return redirect('products:productkit-list')
|
||||
except Exception as e:
|
||||
messages.error(self.request, f'Ошибка при сохранении: {str(e)}')
|
||||
return self.form_invalid(form)
|
||||
else:
|
||||
# Если формсет невалиден, показываем форму с ошибками
|
||||
messages.error(self.request, 'Пожалуйста, исправьте ошибки в компонентах комплекта.')
|
||||
return self.form_invalid(form)
|
||||
|
||||
def form_invalid(self, form):
|
||||
# Получаем формсет для отображения ошибок
|
||||
context = self.get_context_data(form=form)
|
||||
return self.render_to_response(context)
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse_lazy('products:productkit-list')
|
||||
|
||||
|
||||
class ProductKitDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView):
|
||||
"""
|
||||
View для просмотра деталей комплекта.
|
||||
Показывает все компоненты, цены, фотографии.
|
||||
"""
|
||||
model = ProductKit
|
||||
template_name = 'products/productkit_detail.html'
|
||||
context_object_name = 'kit'
|
||||
permission_required = 'products.view_productkit'
|
||||
|
||||
def get_queryset(self):
|
||||
# Prefetch для оптимизации запросов
|
||||
return super().get_queryset().prefetch_related(
|
||||
'photos',
|
||||
'kit_items__product',
|
||||
'kit_items__variant_group',
|
||||
'tags'
|
||||
)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
# Добавляем фотографии комплекта в контекст
|
||||
context['productkit_photos'] = self.object.photos.all().order_by('order', 'created_at')
|
||||
context['photos_count'] = self.object.photos.count()
|
||||
# Добавляем компоненты
|
||||
context['kit_items'] = self.object.kit_items.all().select_related('product', 'variant_group')
|
||||
return context
|
||||
|
||||
|
||||
class ProductKitDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteView):
|
||||
"""
|
||||
View для удаления комплекта.
|
||||
"""
|
||||
model = ProductKit
|
||||
template_name = 'products/productkit_confirm_delete.html'
|
||||
context_object_name = 'kit'
|
||||
permission_required = 'products.delete_productkit'
|
||||
|
||||
def get_success_url(self):
|
||||
messages.success(self.request, f'Комплект "{self.object.name}" успешно удален!')
|
||||
return reverse_lazy('products:productkit-list')
|
||||
73
myproject/products/views/utils.py
Normal file
73
myproject/products/views/utils.py
Normal file
@@ -0,0 +1,73 @@
|
||||
"""
|
||||
Утилиты для работы с фотографиями товаров, комплектов и категорий.
|
||||
"""
|
||||
import os
|
||||
from django.db import models
|
||||
|
||||
|
||||
def validate_photo(photo):
|
||||
"""
|
||||
Валидация загружаемого фото.
|
||||
Возвращает (True, None) если валидно, или (False, error_message) если ошибка.
|
||||
"""
|
||||
max_size = 5 * 1024 * 1024 # 5MB
|
||||
allowed_extensions = ['.jpg', '.jpeg', '.png', '.gif', '.webp']
|
||||
|
||||
if photo.size > max_size:
|
||||
return False, f'Размер файла {photo.name} превышает 5MB.'
|
||||
|
||||
ext = os.path.splitext(photo.name)[1].lower()
|
||||
if ext not in allowed_extensions:
|
||||
return False, f'Формат файла {ext} не поддерживается. Разрешены: {", ".join(allowed_extensions)}'
|
||||
|
||||
return True, None
|
||||
|
||||
|
||||
def handle_photos(request, parent_obj, photo_model, parent_field_name):
|
||||
"""
|
||||
Универсальная обработка загружаемых фотографий.
|
||||
|
||||
Args:
|
||||
request: HTTP request с FILES
|
||||
parent_obj: Родительский объект (Product, ProductKit или ProductCategory)
|
||||
photo_model: Модель фотографии (ProductPhoto, ProductKitPhoto, ProductCategoryPhoto)
|
||||
parent_field_name: Имя поля связи в модели фото ('product', 'kit', 'category')
|
||||
|
||||
Returns:
|
||||
Список сообщений об ошибках (пустой список если все ок).
|
||||
"""
|
||||
errors = []
|
||||
photos = request.FILES.getlist('photos')
|
||||
|
||||
if not photos:
|
||||
return errors
|
||||
|
||||
if len(photos) > 10:
|
||||
errors.append('Можно загрузить не более 10 фотографий.')
|
||||
return errors
|
||||
|
||||
# Получаем максимальный order для этого родительского объекта
|
||||
filter_kwargs = {parent_field_name: parent_obj}
|
||||
max_order = photo_model.objects.filter(**filter_kwargs).aggregate(
|
||||
models.Max('order')
|
||||
)['order__max']
|
||||
|
||||
# Если фото нет, начинаем с 0, иначе с max_order + 1
|
||||
next_order = 0 if max_order is None else max_order + 1
|
||||
|
||||
# Валидация и сохранение фото
|
||||
for photo in photos:
|
||||
is_valid, error_msg = validate_photo(photo)
|
||||
if not is_valid:
|
||||
errors.append(error_msg)
|
||||
else:
|
||||
# Создаем фото с правильной связью
|
||||
create_kwargs = {
|
||||
parent_field_name: parent_obj,
|
||||
'image': photo,
|
||||
'order': next_order
|
||||
}
|
||||
photo_model.objects.create(**create_kwargs)
|
||||
next_order += 1
|
||||
|
||||
return errors
|
||||
Reference in New Issue
Block a user