From ffc3b0c42d06a142584b773189d645095cc736b4 Mon Sep 17 00:00:00 2001 From: Andrey Smakotin Date: Mon, 1 Dec 2025 22:44:36 +0300 Subject: [PATCH] feat: implement role-based permissions for product views MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- .../products/views/configurablekit_views.py | 13 +++--- myproject/products/views/photo_management.py | 1 - myproject/products/views/product_views.py | 21 ++++------ myproject/products/views/productkit_views.py | 21 ++++------ myproject/user_roles/mixins.py | 42 +++++++++++++++++++ 5 files changed, 65 insertions(+), 33 deletions(-) diff --git a/myproject/products/views/configurablekit_views.py b/myproject/products/views/configurablekit_views.py index 15e8d11..b9b9519 100644 --- a/myproject/products/views/configurablekit_views.py +++ b/myproject/products/views/configurablekit_views.py @@ -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') diff --git a/myproject/products/views/photo_management.py b/myproject/products/views/photo_management.py index 1b88e5c..68da6c4 100644 --- a/myproject/products/views/photo_management.py +++ b/myproject/products/views/photo_management.py @@ -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 diff --git a/myproject/products/views/product_views.py b/myproject/products/views/product_views.py index ae32108..3d065ef 100644 --- a/myproject/products/views/product_views.py +++ b/myproject/products/views/product_views.py @@ -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): diff --git a/myproject/products/views/productkit_views.py b/myproject/products/views/productkit_views.py index 13e4c5d..11e425a 100644 --- a/myproject/products/views/productkit_views.py +++ b/myproject/products/views/productkit_views.py @@ -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): diff --git a/myproject/user_roles/mixins.py b/myproject/user_roles/mixins.py index 5e53895..347f403 100644 --- a/myproject/user_roles/mixins.py +++ b/myproject/user_roles/mixins.py @@ -1,4 +1,5 @@ from django.contrib import admin +from django.contrib.auth.mixins import AccessMixin from django.core.exceptions import PermissionDenied from user_roles.services import RoleService @@ -92,3 +93,44 @@ class OwnerOnlyAdminMixin(RoleBasedAdminMixin): class ManagerOwnerAdminMixin(RoleBasedAdminMixin): """Миксин для админки, доступной менеджеру и владельцу""" required_roles = ['owner', 'manager'] + + +# Миксины для обычных views (не админки) + +class RoleRequiredMixin(AccessMixin): + """ + Миксин для проверки ролей в обычных CBV. + + Использование: + class MyView(RoleRequiredMixin, ListView): + required_roles = ['owner', 'manager'] + """ + required_roles = [] # Список ролей, которым разрешен доступ + + def dispatch(self, request, *args, **kwargs): + if not request.user.is_authenticated: + return self.handle_no_permission() + + # Superuser имеет полный доступ + if request.user.is_superuser: + return super().dispatch(request, *args, **kwargs) + + # Если роли не указаны, доступ разрешен всем аутентифицированным + if not self.required_roles: + return super().dispatch(request, *args, **kwargs) + + # Проверяем роль пользователя + if not RoleService.user_has_role(request.user, *self.required_roles): + raise PermissionDenied("У вас нет прав для доступа к этой странице") + + return super().dispatch(request, *args, **kwargs) + + +class OwnerRequiredMixin(RoleRequiredMixin): + """Миксин для view, доступного только владельцу""" + required_roles = ['owner'] + + +class ManagerOwnerRequiredMixin(RoleRequiredMixin): + """Миксин для view, доступного менеджеру и владельцу""" + required_roles = ['owner', 'manager']