+
+
+ Читать далее
+
+
+ {% 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()
+"""
diff --git a/backend/requirements.txt b/backend/requirements.txt
index 61d34542..5a6a286b 100644
--- a/backend/requirements.txt
+++ b/backend/requirements.txt
@@ -102,6 +102,7 @@ qrcode==8.1
redis==6.4.0
requests==2.32.4
requests-oauthlib==2.0.0
+rules==3.5
service-identity==24.2.0
six==1.17.0
social-auth-app-django==5.5.1
diff --git a/docs/guides/DJANGO_RULES_INTEGRATION.md b/docs/guides/DJANGO_RULES_INTEGRATION.md
new file mode 100644
index 00000000..d1d7a7be
--- /dev/null
+++ b/docs/guides/DJANGO_RULES_INTEGRATION.md
@@ -0,0 +1,527 @@
+# Интеграция django-rules: Object-Level Permissions
+
+**Дата:** 28 февраля 2026
+**Статус:** ✅ Базовая интеграция завершена
+**Ветка:** `feature/django-rules`
+
+## Обзор
+
+django-rules — это библиотека для декларативного управления правами доступа на уровне объектов в Django. Она позволяет определять сложную логику permissions через композицию простых предикатов.
+
+### Преимущества
+
+- ✅ **Object-level permissions**: проверка прав на конкретные объекты
+- ✅ **Декларативный синтаксис**: правила определяются явно и читаемо
+- ✅ **Композиция**: комбинирование предикатов через `&`, `|`, `~`
+- ✅ **Интеграция**: работает с Django, DRF, Admin, Templates
+- ✅ **Производительность**: легковесная библиотека без overhead
+- ✅ **Тестируемость**: правила легко покрываются unit-тестами
+
+### Ссылки
+
+- **GitHub**: https://github.com/dfunckt/django-rules
+- **Документация**: https://github.com/dfunckt/django-rules#rules
+- **PyPI**: https://pypi.org/project/rules/
+
+---
+
+## Установка и настройка
+
+### 1. Установка пакета
+
+```bash
+../.venv/Scripts/pip install rules==3.5
+```
+
+Добавлено в `requirements.txt`:
+```
+rules==3.5
+```
+
+### 2. Настройка Django
+
+**settings.py**:
+
+```python
+INSTALLED_APPS = [
+ # ...
+ "rules", # django-rules для декларативных permissions
+ # ...
+]
+
+AUTHENTICATION_BACKENDS = [
+ "eusrr_backend.auth_backends.LDAP3Backend",
+ "eusrr_backend.auth_backends.EmailOrPhoneBackend",
+ "eusrr_backend.auth_backends.SuperuserOnlyBackend",
+ "eusrr_backend.auth_backends.PositionRoleBackend",
+ "rules.permissions.ObjectPermissionBackend", # ← django-rules backend
+ "django.contrib.auth.backends.ModelBackend",
+]
+```
+
+### 3. Создание файлов правил
+
+Созданы базовые правила для двух приложений:
+
+- **employees/rules.py**: правила доступа к профилям сотрудников
+- **documents/rules.py**: правила доступа к документам
+
+---
+
+## Архитектура правил
+
+### Базовая структура
+
+```python
+import rules
+
+# 1. ПРЕДИКАТЫ - базовые проверки (возвращают True/False)
+@rules.predicate
+def is_document_owner(user, document):
+ return document.owner == user
+
+# 2. ПРАВИЛА - композиция предикатов
+rules.add_rule(
+ 'documents.view_document',
+ is_superuser | is_document_owner | has_document_access
+)
+
+# 3. PERMISSIONS (опционально) - регистрация в Django permissions
+rules.add_perm('documents.view_document', is_superuser | is_document_owner)
+```
+
+### Композиция предикатов
+
+```python
+# Логическое И (AND)
+rule = predicate1 & predicate2
+
+# Логическое ИЛИ (OR)
+rule = predicate1 | predicate2
+
+# Логическое НЕ (NOT)
+rule = ~predicate1
+
+# Сложные комбинации
+rule = (is_owner | is_admin) & ~is_banned & has_subscription
+```
+
+---
+
+## Реализованные правила
+
+### Employees (employees/rules.py)
+
+#### Предикаты:
+- `is_superuser` — пользователь является суперюзером
+- `is_staff` — пользователь имеет статус персонала
+- `is_hr_staff` — HR сотрудник (по должности)
+- `is_department_head` — руководитель отдела
+- `is_own_profile` — собственный профиль
+- `is_same_department` — сотрудники в одном отделе
+
+#### Правила:
+- `employees.view_employee` — просмотр профиля
+- `employees.change_employee` — изменение профиля
+- `employees.delete_employee` — удаление сотрудника
+- `employees.view_all_employees` — просмотр списка
+- `employees.view_reports` — просмотр отчётов
+- `employees.change_position` — изменение должности
+- `employees.change_department` — изменение отдела
+- `employees.view_contact_info` — просмотр контактов
+
+### Documents (documents/rules.py)
+
+#### Предикаты:
+- `is_document_owner` — владелец документа
+- `is_document_author` — автор документа
+- `has_document_access` — доступ через is_public/department/shared
+- `can_approve_documents` — может согласовывать (по должности)
+- `is_document_approver` — назначен согласующим
+- `is_same_department` — документ отдела пользователя
+- `is_document_category_manager` — менеджер категории
+
+#### Правила:
+- `documents.view_document` — просмотр документа
+- `documents.change_document` — изменение документа
+- `documents.delete_document` — удаление документа
+- `documents.approve_document` — согласование документа
+- `documents.publish_document` — публикация документа
+- `documents.download_document` — скачивание документа
+- `documents.share_document` — выдача доступа
+- `documents.view_document_history` — просмотр истории
+- `documents.view_department_documents` — документы отдела
+
+---
+
+## Использование
+
+### 1. В Views
+
+```python
+from django.core.exceptions import PermissionDenied
+import rules
+
+def document_detail(request, pk):
+ document = get_object_or_404(Document, pk=pk)
+
+ # Проверка через rules.test_rule
+ if not rules.test_rule('documents.view_document', request.user, document):
+ raise PermissionDenied
+
+ return render(request, 'documents/detail.html', {'document': document})
+
+
+def document_approve(request, pk):
+ document = get_object_or_404(Document, pk=pk)
+
+ if not rules.test_rule('documents.approve_document', request.user, document):
+ raise PermissionDenied
+
+ document.status = 'approved'
+ document.approved_by = request.user
+ document.save()
+
+ return redirect('documents:detail', pk=document.pk)
+```
+
+### 2. В Templates
+
+```django
+{% load rules %}
+
+{% has_rule 'documents.change_document' user document as can_edit %}
+{% if can_edit %}
+
+ Редактировать
+
+{% endif %}
+
+{% has_rule 'documents.approve_document' user document as can_approve %}
+{% if can_approve and document.status == 'pending' %}
+
+{% endif %}
+```
+
+### 3. В DRF Permissions
+
+```python
+from rest_framework import permissions
+import rules
+
+class DocumentPermission(permissions.BasePermission):
+ """Object-level permission для Document через django-rules"""
+
+ def has_object_permission(self, request, view, obj):
+ if request.method in permissions.SAFE_METHODS:
+ return rules.test_rule('documents.view_document', request.user, obj)
+ elif request.method in ['PUT', 'PATCH']:
+ return rules.test_rule('documents.change_document', request.user, obj)
+ elif request.method == 'DELETE':
+ return rules.test_rule('documents.delete_document', request.user, obj)
+ return False
+
+
+# В ViewSet:
+class DocumentViewSet(viewsets.ModelViewSet):
+ queryset = Document.objects.all()
+ serializer_class = DocumentSerializer
+ permission_classes = [permissions.IsAuthenticated, DocumentPermission]
+```
+
+### 4. В Django Admin
+
+```python
+from django.contrib import admin
+import rules
+
+class DocumentAdmin(admin.ModelAdmin):
+ def has_change_permission(self, request, obj=None):
+ if obj is None:
+ # Проверка на уровне модели (list view)
+ return super().has_change_permission(request, obj)
+
+ # Проверка на уровне объекта (change view)
+ return rules.test_rule('documents.change_document', request.user, obj)
+
+ def has_delete_permission(self, request, obj=None):
+ if obj is None:
+ return super().has_delete_permission(request, obj)
+ return rules.test_rule('documents.delete_document', request.user, obj)
+```
+
+### 5. Фильтрация QuerySet
+
+```python
+from django.db.models import Q
+
+def get_accessible_documents(user):
+ """Возвращает документы, к которым пользователь имеет доступ"""
+ return Document.objects.filter(
+ Q(created_by=user) | # Созданные пользователем
+ Q(owner=user) | # Принадлежащие пользователю
+ Q(is_public=True) | # Публичные
+ Q(department_access=user.department) | # Для отдела
+ Q(shared_with=user) # Расшаренные
+ ).distinct()
+```
+
+### 6. Model-level Permissions (опционально)
+
+Если нужно интегрировать с Django permissions (`user.has_perm()`):
+
+```python
+# В rules.py:
+rules.add_perm('documents.view_document', is_superuser | has_document_access)
+rules.add_perm('documents.change_document', is_superuser | is_document_owner)
+
+# Теперь можно использовать стандартный API:
+if request.user.has_perm('documents.view_document', document):
+ # ...
+```
+
+---
+
+## Тестирование
+
+### Unit-тесты для предикатов
+
+```python
+import pytest
+import rules
+from employees.rules import is_own_profile, is_hr_staff
+
+@pytest.mark.django_db
+def test_is_own_profile():
+ user = Employee.objects.create(username='test')
+ other = Employee.objects.create(username='other')
+
+ assert is_own_profile(user, user) is True
+ assert is_own_profile(user, other) is False
+
+
+@pytest.mark.django_db
+def test_is_hr_staff():
+ hr_position = Position.objects.create(name='HR Manager')
+ dev_position = Position.objects.create(name='Developer')
+
+ hr_user = Employee.objects.create(username='hr', position=hr_position)
+ dev_user = Employee.objects.create(username='dev', position=dev_position)
+
+ assert is_hr_staff(hr_user) is True
+ assert is_hr_staff(dev_user) is False
+```
+
+### Интеграционные тесты для правил
+
+```python
+import pytest
+import rules
+
+@pytest.mark.django_db
+def test_view_employee_permission():
+ hr = Employee.objects.create(username='hr', position=hr_position)
+ employee = Employee.objects.create(username='employee')
+
+ # HR может просматривать любого сотрудника
+ assert rules.test_rule('employees.view_employee', hr, employee)
+
+ # Сотрудник может просматривать свой профиль
+ assert rules.test_rule('employees.view_employee', employee, employee)
+```
+
+---
+
+## Следующие шаги
+
+### 1. Адаптация под реальные модели
+
+Текущие правила содержат примеры и комментарии. Необходимо:
+
+- ✅ Проверить наличие полей в моделях (`owner`, `is_public`, `shared_with` и т.д.)
+- ✅ Адаптировать предикаты под реальную структуру
+- ✅ Убрать неиспользуемые предикаты
+- ✅ Добавить недостающие правила
+
+### 2. Миграция существующих permissions
+
+Заменить ручные проверки на правила:
+
+```python
+# Старый код:
+if not (request.user.is_superuser or document.owner == request.user):
+ raise PermissionDenied
+
+# Новый код:
+if not rules.test_rule('documents.change_document', request.user, document):
+ raise PermissionDenied
+```
+
+### 3. Покрытие тестами
+
+Создать тесты для всех критичных правил:
+
+- `employees/tests/test_rules.py`
+- `documents/tests/test_rules.py`
+
+### 4. Расширение на другие приложения
+
+Создать `rules.py` для:
+
+- `calendar_app` — доступ к календарям и событиям
+- `requests_app` — создание и обработка заявок
+- `communications` — доступ к чатам и сообщениям
+- `finance` — финансовые документы и отчёты
+
+### 5. Performance optimization
+
+Для больших выборок использовать фильтрацию на уровне БД:
+
+```python
+# Вместо:
+documents = [d for d in Document.objects.all() if rules.test_rule('view', user, d)]
+
+# Использовать:
+documents = get_accessible_documents(user)
+```
+
+### 6. Документация для команды
+
+- Создать примеры использования для каждого приложения
+- Обновить CODE_STYLE.md с правилами написания predicates
+- Провести код-ревью существующих permissions
+
+---
+
+## Сравнение с django-guardian
+
+| Аспект | django-rules | django-guardian |
+|--------|--------------|-----------------|
+| **Хранение** | В коде (декларативно) | В БД (таблицы permissions) |
+| **Производительность** | Быстрее (нет запросов в БД) | Медленнее (JOIN с таблицами) |
+| **Гибкость** | Высокая (любая логика) | Ограничена (user-object pairs) |
+| **Миграции** | Не требуются | Нужна БД миграция |
+| **Сложность** | Проще | Сложнее |
+| **Масштабирование** | Отлично для ~1K объектов | Лучше для миллионов объектов |
+
+**Вывод**: для EUSRR с умеренным количеством объектов и сложной логикой прав django-rules — оптимальный выбор.
+
+---
+
+## Примеры реальных сценариев
+
+### Сценарий 1: Многоуровневое согласование
+
+```python
+@rules.predicate
+def can_approve_at_current_stage(user, document):
+ """Проверка, что пользователь — текущий согласующий"""
+ current_stage = document.approval_chain.filter(status='pending').first()
+ if not current_stage:
+ return False
+ return current_stage.approver == user
+
+rules.add_rule(
+ 'documents.approve_at_stage',
+ is_superuser | can_approve_at_current_stage
+)
+```
+
+### Сценарий 2: Временные ограничения
+
+```python
+from django.utils import timezone
+from datetime import timedelta
+
+@rules.predicate
+def can_edit_recent_document(user, document):
+ """Редактирование только в течение 24 часов после создания"""
+ if not is_document_owner(user, document):
+ return False
+ return timezone.now() - document.created_at < timedelta(hours=24)
+
+rules.add_rule(
+ 'documents.quick_edit',
+ can_edit_recent_document
+)
+```
+
+### Сценарий 3: Иерархия отделов
+
+```python
+@rules.predicate
+def is_parent_department_head(user, employee):
+ """Руководитель вышестоящего отдела"""
+ if not hasattr(employee, 'department') or not hasattr(employee.department, 'parent'):
+ return False
+ return (
+ hasattr(user, 'department') and
+ user.department == employee.department.parent and
+ is_department_head(user)
+ )
+
+rules.add_rule(
+ 'employees.view_subordinate',
+ is_superuser | is_department_head | is_parent_department_head
+)
+```
+
+---
+
+## Отладка и мониторинг
+
+### Логирование проверок прав
+
+```python
+import logging
+
+logger = logging.getLogger(__name__)
+
+@rules.predicate
+def is_document_owner(user, document):
+ result = document.owner == user
+ logger.debug(
+ f"Permission check: is_document_owner({user.username}, doc_{document.pk}) = {result}"
+ )
+ return result
+```
+
+### Отладочная view для проверки прав
+
+```python
+def debug_permissions(request, document_id):
+ """DEBUG view для проверки прав доступа"""
+ if not settings.DEBUG:
+ raise Http404
+
+ document = get_object_or_404(Document, pk=document_id)
+
+ checks = {
+ 'view': rules.test_rule('documents.view_document', request.user, document),
+ 'change': rules.test_rule('documents.change_document', request.user, document),
+ 'delete': rules.test_rule('documents.delete_document', request.user, document),
+ 'approve': rules.test_rule('documents.approve_document', request.user, document),
+ }
+
+ return JsonResponse(checks)
+```
+
+---
+
+## Заключение
+
+Базовая интеграция django-rules завершена:
+
+- ✅ Пакет установлен и настроен
+- ✅ Созданы примеры правил для employees и documents
+- ✅ Документация и примеры использования готовы
+
+Следующий шаг — адаптация правил под реальную структуру моделей и миграция существующих проверок прав на использование django-rules.
+
+---
+
+**Автор**: GitHub Copilot
+**Дата**: 28 февраля 2026