+
+
+ Читать далее
+
+
+ {% has_rule 'feed.change_post' user post as can_edit %}
+ {% if can_edit %}
+
+ Редактировать
+
+ {% endif %}
+
+ {% has_rule 'feed.delete_post' user post as can_delete %}
+ {% if can_delete %}
+
+ {% endif %}
+
+ {% has_rule 'feed.pin_post' user post as can_pin %}
+ {% if can_pin and not post.is_pinned %}
+
+ {% endif %}
+
+
+ {% endif %}
+{% endfor %}
+
+{# Создание поста #}
+{% has_rule 'feed.create_post' user None as can_create %}
+{% if can_create %}
+
+ Создать пост
+
+{% endif %}
+
+{# Комментарии #}
+{% has_rule 'feed.comment_post' user None as can_comment %}
+{% if can_comment %}
+
+{% endif %}
+
+
+# В DRF permissions:
+from rest_framework import permissions
+import rules
+
+class PostPermission(permissions.BasePermission):
+ def has_permission(self, request, view):
+ if request.method == 'POST':
+ return rules.test_rule('feed.create_post', request.user, None)
+ return True
+
+ def has_object_permission(self, request, view, obj):
+ if request.method in permissions.SAFE_METHODS:
+ return rules.test_rule('feed.view_post', request.user, obj)
+ elif request.method in ['PUT', 'PATCH']:
+ return rules.test_rule('feed.change_post', request.user, obj)
+ elif request.method == 'DELETE':
+ return rules.test_rule('feed.delete_post', request.user, obj)
+ return False
+
+
+class CommentPermission(permissions.BasePermission):
+ def has_permission(self, request, view):
+ if request.method == 'POST':
+ return rules.test_rule('feed.comment_post', request.user, None)
+ return True
+
+ def has_object_permission(self, request, view, obj):
+ if request.method in permissions.SAFE_METHODS:
+ return rules.test_rule('feed.view_comment', request.user, obj)
+ elif request.method in ['PUT', 'PATCH']:
+ return rules.test_rule('feed.change_comment', request.user, obj)
+ elif request.method == 'DELETE':
+ return rules.test_rule('feed.delete_comment', request.user, obj)
+ return False
+
+
+# Фильтрация QuerySet (только доступные посты):
+from django.db.models import Q
+
+def get_accessible_posts(user):
+ return Post.objects.filter(
+ Q(status='published') & ( # Только опубликованные И (публичные ИЛИ для отдела)
+ Q(is_public=True) |
+ Q(department=user.department) |
+ Q(target_departments=user.department)
+ ) |
+ Q(author=user) # + собственные посты (включая черновики)
+ ).distinct().order_by('-created_at')
+
+
+# API endpoint для лайков:
+@api_view(['POST'])
+@permission_classes([IsAuthenticated])
+def like_post(request, pk):
+ post = get_object_or_404(Post, pk=pk)
+
+ if not rules.test_rule('feed.react_to_post', request.user, None):
+ return Response({'error': 'Permission denied'}, status=403)
+
+ like, created = PostLike.objects.get_or_create(post=post, user=request.user)
+
+ if not created:
+ like.delete()
+ return Response({'liked': False, 'likes_count': post.likes.count()})
+
+ return Response({'liked': True, 'likes_count': post.likes.count()})
+"""
diff --git a/backend/procurement/apps.py b/backend/procurement/apps.py
index 56689084..2be13deb 100644
--- a/backend/procurement/apps.py
+++ b/backend/procurement/apps.py
@@ -9,3 +9,4 @@ class ProcurementConfig(AppConfig):
def ready(self):
"""Подключение сигналов при загрузке приложения."""
import procurement.signals # noqa: F401
+ import procurement.rules # django-rules: регистрация предикатов и правил доступа
diff --git a/backend/procurement/rules.py b/backend/procurement/rules.py
new file mode 100644
index 00000000..5d8194c5
--- /dev/null
+++ b/backend/procurement/rules.py
@@ -0,0 +1,354 @@
+"""
+django-rules: декларативные правила доступа для procurement (закупки)
+
+Правила используются для проверки permissions на уровне объектов.
+https://github.com/dfunckt/django-rules
+"""
+
+import rules
+
+
+# -----------------------------------------------------------------------------
+# ПРЕДИКАТЫ (predicates)
+# -----------------------------------------------------------------------------
+
+@rules.predicate
+def is_superuser(user):
+ """Суперпользователь имеет все права"""
+ return user.is_superuser
+
+
+@rules.predicate
+def is_procurement_manager(user):
+ """
+ Пользователь является менеджером по закупкам.
+ Адаптируйте под вашу логику (по должности, группе и т.д.)
+ """
+ if not hasattr(user, 'position'):
+ return False
+
+ position_name = getattr(user.position, 'name', '').lower()
+ return any(keyword in position_name for keyword in [
+ 'закупк', 'снабжен', 'procurement', 'supply'
+ ])
+
+
+@rules.predicate
+def is_purchase_request_author(user, purchase_request):
+ """Пользователь является автором заявки на закупку"""
+ if purchase_request is None:
+ return False
+
+ return (
+ purchase_request.created_by == user or
+ getattr(purchase_request, 'author', None) == user
+ )
+
+
+@rules.predicate
+def is_purchase_request_approver(user, purchase_request):
+ """Пользователь назначен согласующим для заявки на закупку"""
+ if purchase_request is None:
+ return False
+
+ # Проверка через approvers
+ if hasattr(purchase_request, 'approvers'):
+ return user in purchase_request.approvers.all()
+
+ # Проверка через approval_chain
+ if hasattr(purchase_request, 'approval_chain'):
+ return purchase_request.approval_chain.filter(approver=user).exists()
+
+ return False
+
+
+@rules.predicate
+def is_department_purchase_request(user, purchase_request):
+ """Заявка на закупку относится к отделу пользователя"""
+ if purchase_request is None or not hasattr(user, 'department'):
+ return False
+
+ if hasattr(purchase_request, 'department'):
+ return purchase_request.department == user.department
+
+ # Проверка через автора
+ if hasattr(purchase_request, 'created_by') and hasattr(purchase_request.created_by, 'department'):
+ return purchase_request.created_by.department == user.department
+
+ return False
+
+
+@rules.predicate
+def can_approve_purchases(user):
+ """
+ Пользователь может согласовывать закупки.
+ Адаптируйте под вашу логику (по должности, бюджетным лимитам и т.д.)
+ """
+ if not hasattr(user, 'position'):
+ return False
+
+ position_name = getattr(user.position, 'name', '').lower()
+ return any(keyword in position_name for keyword in [
+ 'руководитель', 'начальник', 'директор', 'финансов'
+ ])
+
+
+@rules.predicate
+def is_supplier_contact(user, supplier):
+ """Пользователь является контактным лицом поставщика"""
+ if supplier is None:
+ return False
+
+ if hasattr(supplier, 'contact_person'):
+ return supplier.contact_person == user
+
+ if hasattr(supplier, 'managers'):
+ return user in supplier.managers.all()
+
+ return False
+
+
+@rules.predicate
+def is_contract_manager(user, contract):
+ """Пользователь является менеджером договора"""
+ if contract is None:
+ return False
+
+ if hasattr(contract, 'manager'):
+ return contract.manager == user
+
+ if hasattr(contract, 'responsible_person'):
+ return contract.responsible_person == user
+
+ return False
+
+
+@rules.predicate
+def can_manage_suppliers(user):
+ """Пользователь может управлять поставщиками"""
+ return is_procurement_manager(user) or is_superuser(user)
+
+
+# -----------------------------------------------------------------------------
+# ПРАВИЛА (rules)
+# -----------------------------------------------------------------------------
+
+# Просмотр заявки на закупку
+rules.add_rule(
+ 'procurement.view_purchase_request',
+ is_superuser | is_purchase_request_author | is_procurement_manager |
+ is_purchase_request_approver | can_approve_purchases
+)
+
+# Создание заявки на закупку
+rules.add_rule(
+ 'procurement.create_purchase_request',
+ rules.is_authenticated # Любой сотрудник может создать заявку
+)
+
+# Изменение заявки на закупку
+rules.add_rule(
+ 'procurement.change_purchase_request',
+ is_superuser | is_purchase_request_author | is_procurement_manager
+)
+
+# Удаление заявки на закупку
+rules.add_rule(
+ 'procurement.delete_purchase_request',
+ is_superuser | is_purchase_request_author
+)
+
+# Согласование заявки на закупку
+rules.add_rule(
+ 'procurement.approve_purchase_request',
+ is_superuser | is_purchase_request_approver | can_approve_purchases | is_procurement_manager
+)
+
+# Изменение статуса заявки
+rules.add_rule(
+ 'procurement.change_request_status',
+ is_superuser | is_procurement_manager | can_approve_purchases
+)
+
+# Просмотр поставщика
+rules.add_rule(
+ 'procurement.view_supplier',
+ is_superuser | is_procurement_manager | is_supplier_contact
+)
+
+# Создание/изменение поставщика
+rules.add_rule(
+ 'procurement.manage_supplier',
+ is_superuser | can_manage_suppliers
+)
+
+# Удаление поставщика
+rules.add_rule(
+ 'procurement.delete_supplier',
+ is_superuser
+)
+
+# Просмотр договора
+rules.add_rule(
+ 'procurement.view_contract',
+ is_superuser | is_contract_manager | is_procurement_manager
+)
+
+# Создание/изменение договора
+rules.add_rule(
+ 'procurement.manage_contract',
+ is_superuser | is_contract_manager | can_manage_suppliers
+)
+
+# Удаление договора
+rules.add_rule(
+ 'procurement.delete_contract',
+ is_superuser
+)
+
+# Просмотр всех заявок (для менеджеров)
+rules.add_rule(
+ 'procurement.view_all_requests',
+ is_superuser | is_procurement_manager | can_approve_purchases
+)
+
+# Просмотр отчётов по закупкам
+rules.add_rule(
+ 'procurement.view_reports',
+ is_superuser | is_procurement_manager | can_approve_purchases
+)
+
+# Экспорт данных по закупкам
+rules.add_rule(
+ 'procurement.export_data',
+ is_superuser | is_procurement_manager
+)
+
+
+# -----------------------------------------------------------------------------
+# ИСПОЛЬЗОВАНИЕ В КОДЕ
+# -----------------------------------------------------------------------------
+
+"""
+# В views:
+from django.core.exceptions import PermissionDenied
+import rules
+
+def purchase_request_detail(request, pk):
+ purchase_request = get_object_or_404(PurchaseRequest, pk=pk)
+
+ if not rules.test_rule('procurement.view_purchase_request', request.user, purchase_request):
+ raise PermissionDenied("У вас нет доступа к этой заявке")
+
+ return render(request, 'procurement/request_detail.html', {
+ 'request': purchase_request
+ })
+
+
+def approve_purchase_request(request, pk):
+ purchase_request = get_object_or_404(PurchaseRequest, pk=pk)
+
+ if not rules.test_rule('procurement.approve_purchase_request', request.user, purchase_request):
+ return JsonResponse({'error': 'Нет прав на согласование'}, status=403)
+
+ purchase_request.status = 'approved'
+ purchase_request.approved_by = request.user
+ purchase_request.approved_at = timezone.now()
+ purchase_request.save()
+
+ return JsonResponse({'success': True})
+
+
+def supplier_list(request):
+ # Фильтрация: только доступные поставщики
+ if rules.test_rule('procurement.view_all_requests', request.user, None):
+ suppliers = Supplier.objects.all()
+ else:
+ suppliers = Supplier.objects.filter(
+ Q(contact_person=request.user) | Q(managers=request.user)
+ )
+
+ return render(request, 'procurement/suppliers.html', {'suppliers': suppliers})
+
+
+# В templates:
+{% load rules %}
+
+{% has_rule 'procurement.approve_purchase_request' user purchase_request as can_approve %}
+{% if can_approve and purchase_request.status == 'pending' %}
+
+
+{% endif %}
+
+{% has_rule 'procurement.manage_supplier' user None as can_manage %}
+{% if can_manage %}
+
+ Добавить поставщика
+
+{% endif %}
+
+
+# В DRF permissions:
+from rest_framework import permissions
+import rules
+
+class PurchaseRequestPermission(permissions.BasePermission):
+ def has_permission(self, request, view):
+ if request.method == 'POST':
+ return rules.test_rule('procurement.create_purchase_request', request.user, None)
+ return True
+
+ def has_object_permission(self, request, view, obj):
+ if request.method in permissions.SAFE_METHODS:
+ return rules.test_rule('procurement.view_purchase_request', request.user, obj)
+ elif request.method in ['PUT', 'PATCH']:
+ return rules.test_rule('procurement.change_purchase_request', request.user, obj)
+ elif request.method == 'DELETE':
+ return rules.test_rule('procurement.delete_purchase_request', request.user, obj)
+ return False
+
+
+class SupplierPermission(permissions.BasePermission):
+ def has_permission(self, request, view):
+ if request.method == 'POST':
+ return rules.test_rule('procurement.manage_supplier', request.user, None)
+ return True
+
+ def has_object_permission(self, request, view, obj):
+ if request.method in permissions.SAFE_METHODS:
+ return rules.test_rule('procurement.view_supplier', request.user, obj)
+ elif request.method in ['PUT', 'PATCH']:
+ return rules.test_rule('procurement.manage_supplier', request.user, None)
+ elif request.method == 'DELETE':
+ return rules.test_rule('procurement.delete_supplier', request.user, None)
+ return False
+
+
+# Фильтрация QuerySet:
+from django.db.models import Q
+
+def get_accessible_purchase_requests(user):
+ if rules.test_rule('procurement.view_all_requests', user, None):
+ return PurchaseRequest.objects.all()
+
+ return PurchaseRequest.objects.filter(
+ Q(created_by=user) | # Созданные пользователем
+ Q(approvers=user) | # Требующие согласования
+ Q(department=user.department) # Заявки отдела
+ ).distinct()
+
+
+def get_accessible_suppliers(user):
+ if rules.test_rule('procurement.view_all_requests', user, None):
+ return Supplier.objects.all()
+
+ return Supplier.objects.filter(
+ Q(contact_person=user) | Q(managers=user)
+ ).distinct()
+"""
diff --git a/backend/requests_app/apps.py b/backend/requests_app/apps.py
index 53046a89..9efa0562 100644
--- a/backend/requests_app/apps.py
+++ b/backend/requests_app/apps.py
@@ -8,3 +8,4 @@ class RequestsAppConfig(AppConfig):
def ready(self):
# Подключаем signals для уведомлений
import requests_app.notification_signals # noqa
+ import requests_app.rules # django-rules: регистрация предикатов и правил доступа
diff --git a/backend/requests_app/rules.py b/backend/requests_app/rules.py
new file mode 100644
index 00000000..b2a421fe
--- /dev/null
+++ b/backend/requests_app/rules.py
@@ -0,0 +1,270 @@
+"""
+django-rules: декларативные правила доступа для requests_app
+
+Правила используются для проверки permissions на уровне объектов.
+https://github.com/dfunckt/django-rules
+"""
+
+import rules
+
+
+# -----------------------------------------------------------------------------
+# ПРЕДИКАТЫ (predicates)
+# -----------------------------------------------------------------------------
+
+@rules.predicate
+def is_superuser(user):
+ """Суперпользователь имеет все права"""
+ return user.is_superuser
+
+
+@rules.predicate
+def is_request_author(user, request_obj):
+ """Пользователь является автором заявки"""
+ if request_obj is None:
+ return False
+ return request_obj.created_by == user or request_obj.author == user
+
+
+@rules.predicate
+def is_request_assignee(user, request_obj):
+ """Пользователь является исполнителем заявки"""
+ if request_obj is None:
+ return False
+
+ # Проверка через assigned_to (один исполнитель)
+ if hasattr(request_obj, 'assigned_to'):
+ return request_obj.assigned_to == user
+
+ # Проверка через assignees (несколько исполнителей)
+ if hasattr(request_obj, 'assignees'):
+ return user in request_obj.assignees.all()
+
+ return False
+
+
+@rules.predicate
+def is_request_approver(user, request_obj):
+ """Пользователь назначен согласующим для этой заявки"""
+ if request_obj is None:
+ return False
+
+ # Проверка через approvers
+ if hasattr(request_obj, 'approvers'):
+ return user in request_obj.approvers.all()
+
+ # Проверка через approval_chain
+ if hasattr(request_obj, 'approval_chain'):
+ return request_obj.approval_chain.filter(approver=user).exists()
+
+ return False
+
+
+@rules.predicate
+def can_manage_requests(user):
+ """
+ Пользователь может управлять заявками (по должности или роли).
+ Адаптируйте под вашу логику.
+ """
+ if not hasattr(user, 'position'):
+ return False
+
+ position_name = getattr(user.position, 'name', '').lower()
+ return any(keyword in position_name for keyword in [
+ 'руководитель', 'начальник', 'директор', 'менеджер', 'администратор'
+ ])
+
+
+@rules.predicate
+def is_department_request(user, request_obj):
+ """Заявка относится к отделу пользователя"""
+ if request_obj is None or not hasattr(user, 'department'):
+ return False
+
+ # Проверка через department
+ if hasattr(request_obj, 'department'):
+ return request_obj.department == user.department
+
+ # Проверка через автора заявки
+ if hasattr(request_obj, 'created_by') and hasattr(request_obj.created_by, 'department'):
+ return request_obj.created_by.department == user.department
+
+ return False
+
+
+@rules.predicate
+def is_request_watcher(user, request_obj):
+ """Пользователь наблюдает за заявкой (подписан на уведомления)"""
+ if request_obj is None:
+ return False
+
+ if hasattr(request_obj, 'watchers'):
+ return user in request_obj.watchers.all()
+
+ return False
+
+
+@rules.predicate
+def can_change_request_status(user, request_obj):
+ """Пользователь может менять статус заявки"""
+ if request_obj is None:
+ return False
+
+ # Автор может закрывать/отменять свою заявку
+ if is_request_author(user, request_obj):
+ return True
+
+ # Исполнитель может переводить в работу/выполнено
+ if is_request_assignee(user, request_obj):
+ return True
+
+ # Менеджеры могут менять любые статусы
+ if can_manage_requests(user):
+ return True
+
+ return False
+
+
+# -----------------------------------------------------------------------------
+# ПРАВИЛА (rules)
+# -----------------------------------------------------------------------------
+
+# Просмотр заявки
+rules.add_rule(
+ 'requests_app.view_request',
+ is_superuser | is_request_author | is_request_assignee |
+ is_request_approver | is_request_watcher | can_manage_requests
+)
+
+# Изменение заявки
+rules.add_rule(
+ 'requests_app.change_request',
+ is_superuser | is_request_author | can_manage_requests
+)
+
+# Удаление заявки (только автор и менеджеры)
+rules.add_rule(
+ 'requests_app.delete_request',
+ is_superuser | (is_request_author & rules.always_allow) | can_manage_requests
+)
+
+# Назначение исполнителя
+rules.add_rule(
+ 'requests_app.assign_request',
+ is_superuser | can_manage_requests | is_request_author
+)
+
+# Изменение статуса заявки
+rules.add_rule(
+ 'requests_app.change_status',
+ is_superuser | can_change_request_status
+)
+
+# Согласование заявки
+rules.add_rule(
+ 'requests_app.approve_request',
+ is_superuser | is_request_approver | can_manage_requests
+)
+
+# Комментирование заявки
+rules.add_rule(
+ 'requests_app.comment_request',
+ is_superuser | is_request_author | is_request_assignee |
+ is_request_watcher | can_manage_requests
+)
+
+# Добавление наблюдателей
+rules.add_rule(
+ 'requests_app.add_watchers',
+ is_superuser | is_request_author | can_manage_requests
+)
+
+# Просмотр всех заявок отдела
+rules.add_rule(
+ 'requests_app.view_department_requests',
+ is_superuser | can_manage_requests
+)
+
+# Просмотр статистики по заявкам
+rules.add_rule(
+ 'requests_app.view_statistics',
+ is_superuser | can_manage_requests
+)
+
+
+# -----------------------------------------------------------------------------
+# ИСПОЛЬЗОВАНИЕ В КОДЕ
+# -----------------------------------------------------------------------------
+
+"""
+# В views:
+from django.core.exceptions import PermissionDenied
+import rules
+
+def request_detail(request, pk):
+ request_obj = get_object_or_404(Request, pk=pk)
+
+ if not rules.test_rule('requests_app.view_request', request.user, request_obj):
+ raise PermissionDenied
+
+ return render(request, 'requests_app/detail.html', {'request': request_obj})
+
+
+def request_assign(request, pk):
+ request_obj = get_object_or_404(Request, pk=pk)
+
+ if not rules.test_rule('requests_app.assign_request', request.user, request_obj):
+ raise PermissionDenied
+
+ # Логика назначения
+ assignee_id = request.POST.get('assignee_id')
+ request_obj.assigned_to_id = assignee_id
+ request_obj.save()
+
+ return redirect('requests_app:detail', pk=request_obj.pk)
+
+
+# В templates:
+{% load rules %}
+
+{% has_rule 'requests_app.change_status' user request as can_change_status %}
+{% if can_change_status %}
+
+{% endif %}
+
+
+# В DRF permissions:
+from rest_framework import permissions
+import rules
+
+class RequestPermission(permissions.BasePermission):
+ def has_object_permission(self, request, view, obj):
+ if request.method in permissions.SAFE_METHODS:
+ return rules.test_rule('requests_app.view_request', request.user, obj)
+ elif request.method in ['PUT', 'PATCH']:
+ return rules.test_rule('requests_app.change_request', request.user, obj)
+ elif request.method == 'DELETE':
+ return rules.test_rule('requests_app.delete_request', request.user, obj)
+ return False
+
+
+# Фильтрация QuerySet (только доступные заявки):
+from django.db.models import Q
+
+def get_accessible_requests(user):
+ return Request.objects.filter(
+ Q(created_by=user) | # Созданные пользователем
+ Q(assigned_to=user) | # Назначенные пользователю
+ Q(watchers=user) | # Наблюдаемые пользователем
+ Q(approvers=user) | # Требующие согласования
+ Q(department=user.department) # Заявки отдела
+ ).distinct()
+"""