diff --git a/myproject/products/forms.py b/myproject/products/forms.py index a24dada..787c9bf 100644 --- a/myproject/products/forms.py +++ b/myproject/products/forms.py @@ -16,7 +16,7 @@ class ProductForm(forms.ModelForm): ) tags = forms.ModelMultipleChoiceField( - queryset=ProductTag.objects.all(), + queryset=ProductTag.objects.filter(is_active=True), widget=forms.CheckboxSelectMultiple, required=False, label="Теги" @@ -82,7 +82,7 @@ class ProductKitForm(forms.ModelForm): ) tags = forms.ModelMultipleChoiceField( - queryset=ProductTag.objects.all(), + queryset=ProductTag.objects.filter(is_active=True), widget=forms.CheckboxSelectMultiple, required=False, label="Теги" diff --git a/myproject/products/templates/products/product_detail.html b/myproject/products/templates/products/product_detail.html index df6947d..0581c07 100644 --- a/myproject/products/templates/products/product_detail.html +++ b/myproject/products/templates/products/product_detail.html @@ -154,7 +154,9 @@ {% if product.tags.all %} {% for tag in product.tags.all %} - {{ tag.name }} + {% if tag.is_active %} + {{ tag.name }} + {% endif %} {% endfor %} {% else %} - diff --git a/myproject/products/templates/products/productkit_detail.html b/myproject/products/templates/products/productkit_detail.html index 1fa135b..f20a86c 100644 --- a/myproject/products/templates/products/productkit_detail.html +++ b/myproject/products/templates/products/productkit_detail.html @@ -104,7 +104,9 @@
Теги:
{% for tag in kit.tags.all %} + {% if tag.is_active %} {{ tag.name }} + {% endif %} {% endfor %}
diff --git a/myproject/products/templates/products/tag_list.html b/myproject/products/templates/products/tag_list.html index 5de779a..1d6f738 100644 --- a/myproject/products/templates/products/tag_list.html +++ b/myproject/products/templates/products/tag_list.html @@ -87,11 +87,13 @@ {{ tag.kits_count }} - {% if tag.is_active %} - Активен - {% else %} - Неактивен - {% endif %} +
+ +
@@ -241,6 +243,45 @@ document.addEventListener('DOMContentLoaded', function() { } }); }); + +// Обработчик переключения статуса тега +document.querySelectorAll('.tag-status-switch').forEach(toggle => { + toggle.addEventListener('click', async function(e) { + e.preventDefault(); // Предотвращаем стандартное поведение checkbox + + const tagId = this.dataset.tagId; + const toggleSwitch = this; + const wasChecked = toggleSwitch.checked; // Сохраняем ТЕКУЩЕЕ состояние + + try { + // Отправляем AJAX запрос + const apiUrl = `/products/api/tags/${tagId}/toggle/`; + const response = await fetch(apiUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': '{{ csrf_token }}' + } + }); + + const data = await response.json(); + + if (data.success) { + // Устанавливаем переключатель в НОВОЕ состояние согласно ответу сервера + toggleSwitch.checked = data.is_active; + + // Показываем сообщение об успехе + showMessage(data.message, 'success'); + } else { + // Переключатель остаётся в исходном состоянии (мы его не меняли) + showMessage(data.error || 'Ошибка при обновлении тега', 'danger'); + } + } catch (error) { + // При ошибке сети - переключатель остаётся как был + showMessage('Ошибка сети: ' + error.message, 'danger'); + } + }); +}); {% endblock %} diff --git a/myproject/products/urls.py b/myproject/products/urls.py index daa3b19..c5ebc2c 100644 --- a/myproject/products/urls.py +++ b/myproject/products/urls.py @@ -1,5 +1,6 @@ from django.urls import path from . import views +from .views import api_views app_name = 'products' @@ -37,7 +38,8 @@ urlpatterns = [ # API endpoints path('api/search-products-variants/', views.search_products_and_variants, name='api-search-products-variants'), path('api/kits/temporary/create/', views.create_temporary_kit_api, name='api-temporary-kit-create'), - path('api/tags/create/', views.create_tag_api, name='api-tag-create'), + path('api/tags/create/', api_views.create_tag_api, name='api-tag-create'), + path('api/tags//toggle/', api_views.toggle_tag_status_api, name='api-tag-toggle'), # CRUD URLs for ProductVariantGroup (Варианты товаров) path('variant-groups/', views.ProductVariantGroupListView.as_view(), name='variantgroup-list'), diff --git a/myproject/products/views/api_views.py b/myproject/products/views/api_views.py index cb0bf68..677485b 100644 --- a/myproject/products/views/api_views.py +++ b/myproject/products/views/api_views.py @@ -98,7 +98,27 @@ def search_products_and_variants(request): }], 'pagination': {'more': False} }) - except (Product.DoesNotExist, ProductKit.DoesNotExist, ValueError): + elif item_type == 'variant': + # Для групп вариантов: получаем по ID с prefetch приоритетов + variant = ProductVariantGroup.objects.prefetch_related( + 'items__product' + ).get(id=numeric_id) + + variant_price = variant.price or 0 + count = variant.items.count() + + return JsonResponse({ + 'results': [{ + 'id': variant.id, + 'text': f"{variant.name} ({count} вариантов)", + 'price': str(variant_price), + 'actual_price': str(variant_price), + 'type': 'variant', + 'count': count + }], + 'pagination': {'more': False} + }) + except (Product.DoesNotExist, ProductKit.DoesNotExist, ProductVariantGroup.DoesNotExist, ValueError): return JsonResponse({'results': [], 'pagination': {'more': False}}) query = request.GET.get('q', '').strip() @@ -337,15 +357,18 @@ def search_products_and_variants(request): variants = ProductVariantGroup.objects.filter( models.Q(name__icontains=query) | models.Q(description__icontains=query) - ).prefetch_related('products')[:page_size] + ).prefetch_related('items__product')[:page_size] for variant in variants: - count = variant.products.filter(is_active=True).count() + count = variant.items.count() + variant_price = variant.price or 0 variant_results.append({ 'id': variant.id, 'text': f"{variant.name} ({count} вариантов)", 'type': 'variant', - 'count': count + 'count': count, + 'price': str(variant_price), + 'actual_price': str(variant_price) }) # Формируем финальный результат с группировкой или без @@ -711,3 +734,59 @@ def create_tag_api(request): 'success': False, 'error': f'Ошибка при создании тега: {str(e)}' }, status=500) + + +def toggle_tag_status_api(request, pk): + """ + AJAX endpoint для переключения статуса активности тега. + + Принимает POST запрос и переключает is_active на противоположное значение. + + Возвращает JSON: + { + "success": true, + "is_active": true/false, + "message": "Тег активирован" / "Тег деактивирован" + } + + Или при ошибке: + { + "success": false, + "error": "Описание ошибки" + } + """ + if request.method != 'POST': + return JsonResponse({ + 'success': False, + 'error': 'Метод не поддерживается' + }, status=405) + + try: + from ..models import ProductTag + + # Получаем тег + tag = ProductTag.objects.get(pk=pk) + + # Переключаем статус + tag.is_active = not tag.is_active + tag.save() + + # Определяем сообщение + message = "Тег активирован" if tag.is_active else "Тег деактивирован" + + return JsonResponse({ + 'success': True, + 'is_active': tag.is_active, + 'message': message + }) + + except ProductTag.DoesNotExist: + return JsonResponse({ + 'success': False, + 'error': 'Тег не найден' + }, status=404) + except Exception as e: + return JsonResponse({ + 'success': False, + 'error': f'Ошибка при обновлении тега: {str(e)}' + }, status=500) diff --git a/myproject/products/views/product_views.py b/myproject/products/views/product_views.py index 5416224..d5a149c 100644 --- a/myproject/products/views/product_views.py +++ b/myproject/products/views/product_views.py @@ -63,7 +63,7 @@ class ProductListView(LoginRequiredMixin, PermissionRequiredMixin, ListView): # Данные для фильтров context['filters'] = { 'categories': ProductCategory.objects.filter(is_active=True), - 'tags': ProductTag.objects.all(), + 'tags': ProductTag.objects.filter(is_active=True), 'current': { 'search': self.request.GET.get('search', ''), 'category': self.request.GET.get('category', ''), diff --git a/myproject/products/views/productkit_views.py b/myproject/products/views/productkit_views.py index 8c867c3..eb89591 100644 --- a/myproject/products/views/productkit_views.py +++ b/myproject/products/views/productkit_views.py @@ -8,7 +8,7 @@ from django.urls import reverse_lazy from django.shortcuts import redirect from django.db import transaction -from ..models import ProductKit, ProductCategory, ProductTag, ProductKitPhoto +from ..models import ProductKit, ProductCategory, ProductTag, ProductKitPhoto, Product, ProductVariantGroup from ..forms import ProductKitForm, KitItemFormSetCreate, KitItemFormSetUpdate from .utils import handle_photos @@ -52,7 +52,7 @@ class ProductKitListView(LoginRequiredMixin, PermissionRequiredMixin, ListView): # Данные для фильтров context['filters'] = { 'categories': ProductCategory.objects.filter(is_active=True), - 'tags': ProductTag.objects.all(), + 'tags': ProductTag.objects.filter(is_active=True), 'current': { 'search': self.request.GET.get('search', ''), 'category': self.request.GET.get('category', ''), @@ -146,10 +146,20 @@ class ProductKitCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateVi if '-variant_group' in key and value: try: - variant_group = ProductVariantGroup.objects.get(id=value) + variant_group = ProductVariantGroup.objects.prefetch_related( + 'items__product' + ).get(id=value) + + variant_price = variant_group.price or 0 + count = variant_group.items.count() + selected_variants[key] = { 'id': variant_group.id, - 'text': variant_group.name + 'text': f"{variant_group.name} ({count} вариантов)", + 'price': str(variant_price), + 'actual_price': str(variant_price), + 'type': 'variant', + 'count': count } except ProductVariantGroup.DoesNotExist: pass @@ -248,9 +258,98 @@ class ProductKitUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateVi if self.request.POST: context['kititem_formset'] = KitItemFormSetUpdate(self.request.POST, instance=self.object, prefix='kititem') + + # При ошибке валидации - подготавливаем данные для Select2 + selected_products = {} + selected_variants = {} + + for key, value in self.request.POST.items(): + if '-product' in key and value: + try: + # Очищаем ID от префикса если есть + numeric_value = value.split('_')[1] if '_' in value else value + product = Product.objects.get(id=numeric_value) + + text = product.name + if product.sku: + text += f" ({product.sku})" + + actual_price = product.sale_price if product.sale_price else product.price + selected_products[key] = { + 'id': product.id, + 'text': text, + 'price': str(product.price) if product.price else None, + 'actual_price': str(actual_price) if actual_price else '0' + } + except Product.DoesNotExist: + pass + + if '-variant_group' in key and value: + try: + variant_group = ProductVariantGroup.objects.prefetch_related( + 'items__product' + ).get(id=value) + + variant_price = variant_group.price or 0 + count = variant_group.items.count() + + selected_variants[key] = { + 'id': variant_group.id, + 'text': f"{variant_group.name} ({count} вариантов)", + 'price': str(variant_price), + 'actual_price': str(variant_price), + 'type': 'variant', + 'count': count + } + except ProductVariantGroup.DoesNotExist: + pass + + context['selected_products'] = selected_products + context['selected_variants'] = selected_variants else: context['kititem_formset'] = KitItemFormSetUpdate(instance=self.object, prefix='kititem') + # Подготавливаем данные для предзагрузки в Select2 + selected_products = {} + selected_variants = {} + + for item in self.object.kit_items.all(): + form_prefix = f"kititem-{item.id}" + + if item.product: + product = item.product + text = product.name + if product.sku: + text += f" ({product.sku})" + + actual_price = product.sale_price if product.sale_price else product.price + selected_products[f"{form_prefix}-product"] = { + 'id': product.id, + 'text': text, + 'price': str(product.price) if product.price else None, + 'actual_price': str(actual_price) if actual_price else '0' + } + + if item.variant_group: + variant_group = ProductVariantGroup.objects.prefetch_related( + 'items__product' + ).get(id=item.variant_group.id) + + variant_price = variant_group.price or 0 + count = variant_group.items.count() + + selected_variants[f"{form_prefix}-variant_group"] = { + 'id': variant_group.id, + 'text': f"{variant_group.name} ({count} вариантов)", + 'price': str(variant_price), + 'actual_price': str(variant_price), + 'type': 'variant', + 'count': count + } + + context['selected_products'] = selected_products + context['selected_variants'] = selected_variants + context['productkit_photos'] = self.object.photos.all().order_by('order', 'created_at') context['photos_count'] = self.object.photos.count() diff --git a/myproject/products/views/tag_views.py b/myproject/products/views/tag_views.py index 0df5401..f433c45 100644 --- a/myproject/products/views/tag_views.py +++ b/myproject/products/views/tag_views.py @@ -62,8 +62,15 @@ class ProductTagDetailView(LoginRequiredMixin, DetailView): tag = self.get_object() # Получаем товары и комплекты с этим тегом - context['products'] = tag.products.filter(is_active=True).order_by('name')[:20] - context['kits'] = tag.kits.filter(is_active=True).order_by('name')[:20] + # Если тег неактивный, показываем ВСЕ товары/комплекты (для возможности очистки) + # Если тег активный, показываем только активные товары/комплекты + if tag.is_active: + context['products'] = tag.products.filter(is_active=True).order_by('name')[:20] + context['kits'] = tag.kits.filter(is_active=True).order_by('name')[:20] + else: + context['products'] = tag.products.all().order_by('name')[:20] + context['kits'] = tag.kits.all().order_by('name')[:20] + context['total_products'] = tag.products.count() context['total_kits'] = tag.kits.count()