feat: implement role-based permissions for product views

- Add view mixins (RoleRequiredMixin, OwnerRequiredMixin, ManagerOwnerRequiredMixin) to user_roles/mixins.py
- Replace PermissionRequiredMixin with ManagerOwnerRequiredMixin in all product views
- Remove permission_required attributes from view classes
- Owner and Manager roles now grant access without Django model permissions

This allows owners to access all product functionality through their custom role,
without needing to be superusers or have explicit Django permissions.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-12-01 22:44:36 +03:00
parent 52baf4295f
commit ffc3b0c42d
5 changed files with 65 additions and 33 deletions

View File

@@ -2,7 +2,7 @@
CRUD представления для вариативных товаров (ConfigurableKitProduct).
"""
from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin
from django.contrib.auth.mixins import LoginRequiredMixin
from django.views.generic import ListView, CreateView, DetailView, UpdateView, DeleteView
from django.urls import reverse_lazy
from django.db.models import Q, Prefetch
@@ -12,6 +12,7 @@ from django.views.decorators.http import require_POST
from django.contrib.auth.decorators import login_required
from django.db import transaction
from user_roles.mixins import ManagerOwnerRequiredMixin
from ..models import ConfigurableKitProduct, ConfigurableKitOption, ProductKit, ConfigurableKitProductAttribute
from ..forms import (
ConfigurableKitProductForm,
@@ -22,7 +23,7 @@ from ..forms import (
)
class ConfigurableKitProductListView(LoginRequiredMixin, ListView):
class ConfigurableKitProductListView(LoginRequiredMixin, ManagerOwnerRequiredMixin, ListView):
model = ConfigurableKitProduct
template_name = 'products/configurablekit_list.html'
context_object_name = 'configurable_kits'
@@ -79,7 +80,7 @@ class ConfigurableKitProductListView(LoginRequiredMixin, ListView):
return context
class ConfigurableKitProductDetailView(LoginRequiredMixin, DetailView):
class ConfigurableKitProductDetailView(LoginRequiredMixin, ManagerOwnerRequiredMixin, DetailView):
model = ConfigurableKitProduct
template_name = 'products/configurablekit_detail.html'
context_object_name = 'configurable_kit'
@@ -103,7 +104,7 @@ class ConfigurableKitProductDetailView(LoginRequiredMixin, DetailView):
return context
class ConfigurableKitProductCreateView(LoginRequiredMixin, CreateView):
class ConfigurableKitProductCreateView(LoginRequiredMixin, ManagerOwnerRequiredMixin, CreateView):
model = ConfigurableKitProduct
form_class = ConfigurableKitProductForm
template_name = 'products/configurablekit_form.html'
@@ -375,7 +376,7 @@ class ConfigurableKitProductCreateView(LoginRequiredMixin, CreateView):
return reverse_lazy('products:configurablekit-detail', kwargs={'pk': self.object.pk})
class ConfigurableKitProductUpdateView(LoginRequiredMixin, UpdateView):
class ConfigurableKitProductUpdateView(LoginRequiredMixin, ManagerOwnerRequiredMixin, UpdateView):
model = ConfigurableKitProduct
form_class = ConfigurableKitProductForm
template_name = 'products/configurablekit_form.html'
@@ -652,7 +653,7 @@ class ConfigurableKitProductUpdateView(LoginRequiredMixin, UpdateView):
return reverse_lazy('products:configurablekit-detail', kwargs={'pk': self.object.pk})
class ConfigurableKitProductDeleteView(LoginRequiredMixin, DeleteView):
class ConfigurableKitProductDeleteView(LoginRequiredMixin, ManagerOwnerRequiredMixin, DeleteView):
model = ConfigurableKitProduct
template_name = 'products/configurablekit_confirm_delete.html'
success_url = reverse_lazy('products:configurablekit-list')

View File

@@ -8,7 +8,6 @@ from django.contrib import messages
from django.http import JsonResponse
from django.views.decorators.http import require_http_methods
from django.contrib.auth.decorators import login_required
from django.contrib.auth.mixins import PermissionRequiredMixin
from ..models import ProductPhoto, ProductKitPhoto, ProductCategoryPhoto

View File

@@ -2,7 +2,7 @@
CRUD представления для товаров (Product).
"""
from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin
from django.contrib.auth.mixins import LoginRequiredMixin
from django.views.generic import ListView, CreateView, DetailView, UpdateView, DeleteView
from django.urls import reverse_lazy
from django.db.models import Q, Sum, Value, DecimalField
@@ -13,13 +13,13 @@ from ..models import Product, ProductCategory, ProductTag, ProductKit
from ..forms import ProductForm
from .utils import handle_photos
from ..models import ProductPhoto
from user_roles.mixins import ManagerOwnerRequiredMixin
class ProductListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
class ProductListView(LoginRequiredMixin, ManagerOwnerRequiredMixin, 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):
@@ -109,11 +109,10 @@ class ProductListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
return context
class ProductCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView):
class ProductCreateView(LoginRequiredMixin, ManagerOwnerRequiredMixin, 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:products-list')
@@ -156,11 +155,10 @@ class ProductCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView)
return self.form_invalid(form)
class ProductDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView):
class ProductDetailView(LoginRequiredMixin, ManagerOwnerRequiredMixin, DetailView):
model = Product
template_name = 'products/product_detail.html'
context_object_name = 'product'
permission_required = 'products.view_product'
def get_queryset(self):
# Предзагрузка фотографий и аннотация остатков
@@ -180,11 +178,10 @@ class ProductDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView)
return context
class ProductUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView):
class ProductUpdateView(LoginRequiredMixin, ManagerOwnerRequiredMixin, 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:products-list')
@@ -234,25 +231,23 @@ class ProductUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView)
return self.form_invalid(form)
class ProductDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteView):
class ProductDeleteView(LoginRequiredMixin, ManagerOwnerRequiredMixin, 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:products-list')
class CombinedProductListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
class CombinedProductListView(LoginRequiredMixin, ManagerOwnerRequiredMixin, ListView):
"""
Объединенное представление для товаров и комплектов.
Показывает оба типа продуктов в одном списке с возможностью фильтрации по типу.
"""
template_name = 'products/products_list.html'
context_object_name = 'items'
permission_required = 'products.view_product'
paginate_by = 20
def get_queryset(self):

View File

@@ -2,22 +2,22 @@
CRUD представления для комплектов товаров (ProductKit).
"""
from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin
from django.contrib.auth.mixins import LoginRequiredMixin
from django.views.generic import ListView, CreateView, DetailView, UpdateView, DeleteView
from django.urls import reverse_lazy
from django.shortcuts import redirect
from django.db import transaction, IntegrityError
from user_roles.mixins import ManagerOwnerRequiredMixin
from ..models import ProductKit, ProductCategory, ProductTag, ProductKitPhoto, Product, ProductVariantGroup
from ..forms import ProductKitForm, KitItemFormSetCreate, KitItemFormSetUpdate
from .utils import handle_photos
class ProductKitListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
class ProductKitListView(LoginRequiredMixin, ManagerOwnerRequiredMixin, 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):
@@ -89,14 +89,13 @@ class ProductKitListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
return context
class ProductKitCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView):
class ProductKitCreateView(LoginRequiredMixin, ManagerOwnerRequiredMixin, CreateView):
"""
View для создания нового комплекта с добавлением компонентов на одной странице.
"""
model = ProductKit
form_class = ProductKitForm
template_name = 'products/productkit_create.html'
permission_required = 'products.add_productkit'
def post(self, request, *args, **kwargs):
"""
@@ -248,14 +247,13 @@ class ProductKitCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateVi
return self.render_to_response(context)
class ProductKitUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView):
class ProductKitUpdateView(LoginRequiredMixin, ManagerOwnerRequiredMixin, UpdateView):
"""
View для редактирования существующего комплекта и добавления товаров.
"""
model = ProductKit
form_class = ProductKitForm
template_name = 'products/productkit_edit.html'
permission_required = 'products.change_productkit'
def post(self, request, *args, **kwargs):
"""
@@ -453,7 +451,7 @@ class ProductKitUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateVi
return reverse_lazy('products:products-list')
class ProductKitDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView):
class ProductKitDetailView(LoginRequiredMixin, ManagerOwnerRequiredMixin, DetailView):
"""
View для просмотра деталей комплекта.
Показывает все компоненты, цены, фотографии.
@@ -461,7 +459,6 @@ class ProductKitDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailVi
model = ProductKit
template_name = 'products/productkit_detail.html'
context_object_name = 'kit'
permission_required = 'products.view_productkit'
def get_queryset(self):
# Prefetch для оптимизации запросов
@@ -482,21 +479,20 @@ class ProductKitDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailVi
return context
class ProductKitDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteView):
class ProductKitDeleteView(LoginRequiredMixin, ManagerOwnerRequiredMixin, 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')
class ProductKitMakePermanentView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView):
class ProductKitMakePermanentView(LoginRequiredMixin, ManagerOwnerRequiredMixin, UpdateView):
"""
View для преобразования временного комплекта в постоянный.
Позволяет отредактировать название, добавить категории, теги перед сохранением.
@@ -504,7 +500,6 @@ class ProductKitMakePermanentView(LoginRequiredMixin, PermissionRequiredMixin, U
model = ProductKit
template_name = 'products/productkit_make_permanent.html'
context_object_name = 'kit'
permission_required = 'products.change_productkit'
fields = ['name', 'description', 'categories', 'tags', 'sale_price']
def get_queryset(self):