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:
@@ -2,7 +2,7 @@
|
|||||||
CRUD представления для вариативных товаров (ConfigurableKitProduct).
|
CRUD представления для вариативных товаров (ConfigurableKitProduct).
|
||||||
"""
|
"""
|
||||||
from django.contrib import messages
|
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.views.generic import ListView, CreateView, DetailView, UpdateView, DeleteView
|
||||||
from django.urls import reverse_lazy
|
from django.urls import reverse_lazy
|
||||||
from django.db.models import Q, Prefetch
|
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.contrib.auth.decorators import login_required
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
|
|
||||||
|
from user_roles.mixins import ManagerOwnerRequiredMixin
|
||||||
from ..models import ConfigurableKitProduct, ConfigurableKitOption, ProductKit, ConfigurableKitProductAttribute
|
from ..models import ConfigurableKitProduct, ConfigurableKitOption, ProductKit, ConfigurableKitProductAttribute
|
||||||
from ..forms import (
|
from ..forms import (
|
||||||
ConfigurableKitProductForm,
|
ConfigurableKitProductForm,
|
||||||
@@ -22,7 +23,7 @@ from ..forms import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class ConfigurableKitProductListView(LoginRequiredMixin, ListView):
|
class ConfigurableKitProductListView(LoginRequiredMixin, ManagerOwnerRequiredMixin, ListView):
|
||||||
model = ConfigurableKitProduct
|
model = ConfigurableKitProduct
|
||||||
template_name = 'products/configurablekit_list.html'
|
template_name = 'products/configurablekit_list.html'
|
||||||
context_object_name = 'configurable_kits'
|
context_object_name = 'configurable_kits'
|
||||||
@@ -79,7 +80,7 @@ class ConfigurableKitProductListView(LoginRequiredMixin, ListView):
|
|||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
class ConfigurableKitProductDetailView(LoginRequiredMixin, DetailView):
|
class ConfigurableKitProductDetailView(LoginRequiredMixin, ManagerOwnerRequiredMixin, DetailView):
|
||||||
model = ConfigurableKitProduct
|
model = ConfigurableKitProduct
|
||||||
template_name = 'products/configurablekit_detail.html'
|
template_name = 'products/configurablekit_detail.html'
|
||||||
context_object_name = 'configurable_kit'
|
context_object_name = 'configurable_kit'
|
||||||
@@ -103,7 +104,7 @@ class ConfigurableKitProductDetailView(LoginRequiredMixin, DetailView):
|
|||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
class ConfigurableKitProductCreateView(LoginRequiredMixin, CreateView):
|
class ConfigurableKitProductCreateView(LoginRequiredMixin, ManagerOwnerRequiredMixin, CreateView):
|
||||||
model = ConfigurableKitProduct
|
model = ConfigurableKitProduct
|
||||||
form_class = ConfigurableKitProductForm
|
form_class = ConfigurableKitProductForm
|
||||||
template_name = 'products/configurablekit_form.html'
|
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})
|
return reverse_lazy('products:configurablekit-detail', kwargs={'pk': self.object.pk})
|
||||||
|
|
||||||
|
|
||||||
class ConfigurableKitProductUpdateView(LoginRequiredMixin, UpdateView):
|
class ConfigurableKitProductUpdateView(LoginRequiredMixin, ManagerOwnerRequiredMixin, UpdateView):
|
||||||
model = ConfigurableKitProduct
|
model = ConfigurableKitProduct
|
||||||
form_class = ConfigurableKitProductForm
|
form_class = ConfigurableKitProductForm
|
||||||
template_name = 'products/configurablekit_form.html'
|
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})
|
return reverse_lazy('products:configurablekit-detail', kwargs={'pk': self.object.pk})
|
||||||
|
|
||||||
|
|
||||||
class ConfigurableKitProductDeleteView(LoginRequiredMixin, DeleteView):
|
class ConfigurableKitProductDeleteView(LoginRequiredMixin, ManagerOwnerRequiredMixin, DeleteView):
|
||||||
model = ConfigurableKitProduct
|
model = ConfigurableKitProduct
|
||||||
template_name = 'products/configurablekit_confirm_delete.html'
|
template_name = 'products/configurablekit_confirm_delete.html'
|
||||||
success_url = reverse_lazy('products:configurablekit-list')
|
success_url = reverse_lazy('products:configurablekit-list')
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ from django.contrib import messages
|
|||||||
from django.http import JsonResponse
|
from django.http import JsonResponse
|
||||||
from django.views.decorators.http import require_http_methods
|
from django.views.decorators.http import require_http_methods
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
from django.contrib.auth.mixins import PermissionRequiredMixin
|
|
||||||
|
|
||||||
from ..models import ProductPhoto, ProductKitPhoto, ProductCategoryPhoto
|
from ..models import ProductPhoto, ProductKitPhoto, ProductCategoryPhoto
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
CRUD представления для товаров (Product).
|
CRUD представления для товаров (Product).
|
||||||
"""
|
"""
|
||||||
from django.contrib import messages
|
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.views.generic import ListView, CreateView, DetailView, UpdateView, DeleteView
|
||||||
from django.urls import reverse_lazy
|
from django.urls import reverse_lazy
|
||||||
from django.db.models import Q, Sum, Value, DecimalField
|
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 ..forms import ProductForm
|
||||||
from .utils import handle_photos
|
from .utils import handle_photos
|
||||||
from ..models import ProductPhoto
|
from ..models import ProductPhoto
|
||||||
|
from user_roles.mixins import ManagerOwnerRequiredMixin
|
||||||
|
|
||||||
|
|
||||||
class ProductListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
|
class ProductListView(LoginRequiredMixin, ManagerOwnerRequiredMixin, ListView):
|
||||||
model = Product
|
model = Product
|
||||||
template_name = 'products/product_list.html'
|
template_name = 'products/product_list.html'
|
||||||
context_object_name = 'products'
|
context_object_name = 'products'
|
||||||
permission_required = 'products.view_product'
|
|
||||||
paginate_by = 10
|
paginate_by = 10
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
@@ -109,11 +109,10 @@ class ProductListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
|
|||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
class ProductCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView):
|
class ProductCreateView(LoginRequiredMixin, ManagerOwnerRequiredMixin, CreateView):
|
||||||
model = Product
|
model = Product
|
||||||
form_class = ProductForm
|
form_class = ProductForm
|
||||||
template_name = 'products/product_form.html'
|
template_name = 'products/product_form.html'
|
||||||
permission_required = 'products.add_product'
|
|
||||||
|
|
||||||
def get_success_url(self):
|
def get_success_url(self):
|
||||||
return reverse_lazy('products:products-list')
|
return reverse_lazy('products:products-list')
|
||||||
@@ -156,11 +155,10 @@ class ProductCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView)
|
|||||||
return self.form_invalid(form)
|
return self.form_invalid(form)
|
||||||
|
|
||||||
|
|
||||||
class ProductDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView):
|
class ProductDetailView(LoginRequiredMixin, ManagerOwnerRequiredMixin, DetailView):
|
||||||
model = Product
|
model = Product
|
||||||
template_name = 'products/product_detail.html'
|
template_name = 'products/product_detail.html'
|
||||||
context_object_name = 'product'
|
context_object_name = 'product'
|
||||||
permission_required = 'products.view_product'
|
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
# Предзагрузка фотографий и аннотация остатков
|
# Предзагрузка фотографий и аннотация остатков
|
||||||
@@ -180,11 +178,10 @@ class ProductDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView)
|
|||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
class ProductUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView):
|
class ProductUpdateView(LoginRequiredMixin, ManagerOwnerRequiredMixin, UpdateView):
|
||||||
model = Product
|
model = Product
|
||||||
form_class = ProductForm
|
form_class = ProductForm
|
||||||
template_name = 'products/product_form.html'
|
template_name = 'products/product_form.html'
|
||||||
permission_required = 'products.change_product'
|
|
||||||
|
|
||||||
def get_success_url(self):
|
def get_success_url(self):
|
||||||
return reverse_lazy('products:products-list')
|
return reverse_lazy('products:products-list')
|
||||||
@@ -234,25 +231,23 @@ class ProductUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView)
|
|||||||
return self.form_invalid(form)
|
return self.form_invalid(form)
|
||||||
|
|
||||||
|
|
||||||
class ProductDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteView):
|
class ProductDeleteView(LoginRequiredMixin, ManagerOwnerRequiredMixin, DeleteView):
|
||||||
model = Product
|
model = Product
|
||||||
template_name = 'products/product_confirm_delete.html'
|
template_name = 'products/product_confirm_delete.html'
|
||||||
context_object_name = 'product'
|
context_object_name = 'product'
|
||||||
permission_required = 'products.delete_product'
|
|
||||||
|
|
||||||
def get_success_url(self):
|
def get_success_url(self):
|
||||||
messages.success(self.request, f'Товар "{self.object.name}" успешно удален!')
|
messages.success(self.request, f'Товар "{self.object.name}" успешно удален!')
|
||||||
return reverse_lazy('products:products-list')
|
return reverse_lazy('products:products-list')
|
||||||
|
|
||||||
|
|
||||||
class CombinedProductListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
|
class CombinedProductListView(LoginRequiredMixin, ManagerOwnerRequiredMixin, ListView):
|
||||||
"""
|
"""
|
||||||
Объединенное представление для товаров и комплектов.
|
Объединенное представление для товаров и комплектов.
|
||||||
Показывает оба типа продуктов в одном списке с возможностью фильтрации по типу.
|
Показывает оба типа продуктов в одном списке с возможностью фильтрации по типу.
|
||||||
"""
|
"""
|
||||||
template_name = 'products/products_list.html'
|
template_name = 'products/products_list.html'
|
||||||
context_object_name = 'items'
|
context_object_name = 'items'
|
||||||
permission_required = 'products.view_product'
|
|
||||||
paginate_by = 20
|
paginate_by = 20
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
|
|||||||
@@ -2,22 +2,22 @@
|
|||||||
CRUD представления для комплектов товаров (ProductKit).
|
CRUD представления для комплектов товаров (ProductKit).
|
||||||
"""
|
"""
|
||||||
from django.contrib import messages
|
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.views.generic import ListView, CreateView, DetailView, UpdateView, DeleteView
|
||||||
from django.urls import reverse_lazy
|
from django.urls import reverse_lazy
|
||||||
from django.shortcuts import redirect
|
from django.shortcuts import redirect
|
||||||
from django.db import transaction, IntegrityError
|
from django.db import transaction, IntegrityError
|
||||||
|
|
||||||
|
from user_roles.mixins import ManagerOwnerRequiredMixin
|
||||||
from ..models import ProductKit, ProductCategory, ProductTag, ProductKitPhoto, Product, ProductVariantGroup
|
from ..models import ProductKit, ProductCategory, ProductTag, ProductKitPhoto, Product, ProductVariantGroup
|
||||||
from ..forms import ProductKitForm, KitItemFormSetCreate, KitItemFormSetUpdate
|
from ..forms import ProductKitForm, KitItemFormSetCreate, KitItemFormSetUpdate
|
||||||
from .utils import handle_photos
|
from .utils import handle_photos
|
||||||
|
|
||||||
|
|
||||||
class ProductKitListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
|
class ProductKitListView(LoginRequiredMixin, ManagerOwnerRequiredMixin, ListView):
|
||||||
model = ProductKit
|
model = ProductKit
|
||||||
template_name = 'products/productkit_list.html'
|
template_name = 'products/productkit_list.html'
|
||||||
context_object_name = 'kits'
|
context_object_name = 'kits'
|
||||||
permission_required = 'products.view_productkit'
|
|
||||||
paginate_by = 10
|
paginate_by = 10
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
@@ -89,14 +89,13 @@ class ProductKitListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
|
|||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
class ProductKitCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView):
|
class ProductKitCreateView(LoginRequiredMixin, ManagerOwnerRequiredMixin, CreateView):
|
||||||
"""
|
"""
|
||||||
View для создания нового комплекта с добавлением компонентов на одной странице.
|
View для создания нового комплекта с добавлением компонентов на одной странице.
|
||||||
"""
|
"""
|
||||||
model = ProductKit
|
model = ProductKit
|
||||||
form_class = ProductKitForm
|
form_class = ProductKitForm
|
||||||
template_name = 'products/productkit_create.html'
|
template_name = 'products/productkit_create.html'
|
||||||
permission_required = 'products.add_productkit'
|
|
||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
def post(self, request, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
@@ -248,14 +247,13 @@ class ProductKitCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateVi
|
|||||||
return self.render_to_response(context)
|
return self.render_to_response(context)
|
||||||
|
|
||||||
|
|
||||||
class ProductKitUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView):
|
class ProductKitUpdateView(LoginRequiredMixin, ManagerOwnerRequiredMixin, UpdateView):
|
||||||
"""
|
"""
|
||||||
View для редактирования существующего комплекта и добавления товаров.
|
View для редактирования существующего комплекта и добавления товаров.
|
||||||
"""
|
"""
|
||||||
model = ProductKit
|
model = ProductKit
|
||||||
form_class = ProductKitForm
|
form_class = ProductKitForm
|
||||||
template_name = 'products/productkit_edit.html'
|
template_name = 'products/productkit_edit.html'
|
||||||
permission_required = 'products.change_productkit'
|
|
||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
def post(self, request, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
@@ -453,7 +451,7 @@ class ProductKitUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateVi
|
|||||||
return reverse_lazy('products:products-list')
|
return reverse_lazy('products:products-list')
|
||||||
|
|
||||||
|
|
||||||
class ProductKitDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView):
|
class ProductKitDetailView(LoginRequiredMixin, ManagerOwnerRequiredMixin, DetailView):
|
||||||
"""
|
"""
|
||||||
View для просмотра деталей комплекта.
|
View для просмотра деталей комплекта.
|
||||||
Показывает все компоненты, цены, фотографии.
|
Показывает все компоненты, цены, фотографии.
|
||||||
@@ -461,7 +459,6 @@ class ProductKitDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailVi
|
|||||||
model = ProductKit
|
model = ProductKit
|
||||||
template_name = 'products/productkit_detail.html'
|
template_name = 'products/productkit_detail.html'
|
||||||
context_object_name = 'kit'
|
context_object_name = 'kit'
|
||||||
permission_required = 'products.view_productkit'
|
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
# Prefetch для оптимизации запросов
|
# Prefetch для оптимизации запросов
|
||||||
@@ -482,21 +479,20 @@ class ProductKitDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailVi
|
|||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
class ProductKitDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteView):
|
class ProductKitDeleteView(LoginRequiredMixin, ManagerOwnerRequiredMixin, DeleteView):
|
||||||
"""
|
"""
|
||||||
View для удаления комплекта.
|
View для удаления комплекта.
|
||||||
"""
|
"""
|
||||||
model = ProductKit
|
model = ProductKit
|
||||||
template_name = 'products/productkit_confirm_delete.html'
|
template_name = 'products/productkit_confirm_delete.html'
|
||||||
context_object_name = 'kit'
|
context_object_name = 'kit'
|
||||||
permission_required = 'products.delete_productkit'
|
|
||||||
|
|
||||||
def get_success_url(self):
|
def get_success_url(self):
|
||||||
messages.success(self.request, f'Комплект "{self.object.name}" успешно удален!')
|
messages.success(self.request, f'Комплект "{self.object.name}" успешно удален!')
|
||||||
return reverse_lazy('products:productkit-list')
|
return reverse_lazy('products:productkit-list')
|
||||||
|
|
||||||
|
|
||||||
class ProductKitMakePermanentView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView):
|
class ProductKitMakePermanentView(LoginRequiredMixin, ManagerOwnerRequiredMixin, UpdateView):
|
||||||
"""
|
"""
|
||||||
View для преобразования временного комплекта в постоянный.
|
View для преобразования временного комплекта в постоянный.
|
||||||
Позволяет отредактировать название, добавить категории, теги перед сохранением.
|
Позволяет отредактировать название, добавить категории, теги перед сохранением.
|
||||||
@@ -504,7 +500,6 @@ class ProductKitMakePermanentView(LoginRequiredMixin, PermissionRequiredMixin, U
|
|||||||
model = ProductKit
|
model = ProductKit
|
||||||
template_name = 'products/productkit_make_permanent.html'
|
template_name = 'products/productkit_make_permanent.html'
|
||||||
context_object_name = 'kit'
|
context_object_name = 'kit'
|
||||||
permission_required = 'products.change_productkit'
|
|
||||||
fields = ['name', 'description', 'categories', 'tags', 'sale_price']
|
fields = ['name', 'description', 'categories', 'tags', 'sale_price']
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
from django.contrib.auth.mixins import AccessMixin
|
||||||
from django.core.exceptions import PermissionDenied
|
from django.core.exceptions import PermissionDenied
|
||||||
from user_roles.services import RoleService
|
from user_roles.services import RoleService
|
||||||
|
|
||||||
@@ -92,3 +93,44 @@ class OwnerOnlyAdminMixin(RoleBasedAdminMixin):
|
|||||||
class ManagerOwnerAdminMixin(RoleBasedAdminMixin):
|
class ManagerOwnerAdminMixin(RoleBasedAdminMixin):
|
||||||
"""Миксин для админки, доступной менеджеру и владельцу"""
|
"""Миксин для админки, доступной менеджеру и владельцу"""
|
||||||
required_roles = ['owner', 'manager']
|
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']
|
||||||
|
|||||||
Reference in New Issue
Block a user