feat: добавлены правила для 5 приложений (requests, communications, calendar, procurement, feed)

- requests_app/rules.py: 8 предикатов + 10 правил
  * Предикаты: is_request_author, is_request_assignee, can_manage_requests и др.
  * Правила: view/change/delete/assign/approve requests, комментирование, статистика

- communications/rules.py: 9 предикатов + 15 правил
  * Предикаты: is_chat_member, is_chat_owner, is_chat_admin, is_message_author и др.
  * Правила: view/send/change/delete чаты и сообщения, управление участниками, закрепление
  * Включены примеры для WebSocket consumers

- calendar_app/rules.py: 9 предикатов + 12 правил
  * Предикаты: is_calendar_owner, is_personal_calendar, has_calendar_access и др.
  * Правила: view/change/delete календари и события, подписки, экспорт iCal, приглашения

- procurement/rules.py: 8 предикатов + 13 правил
  * Предикаты: is_procurement_manager, is_purchase_request_author, can_approve_purchases и др.
  * Правила: view/change/delete заявки на закупку, управление поставщиками и договорами

- feed/rules.py: 8 предикатов + 11 правил
  * Предикаты: is_post_author, can_publish_posts, can_moderate_posts и др.
  * Правила: view/create/change/delete посты, публикация, закрепление, комментирование, реакции

Во всех apps.py добавлен импорт rules для автозагрузки правил при старте Django
This commit is contained in:
igor_nadein 2026-02-28 00:13:46 +03:00
parent d3b648b438
commit 3e672b2edc
10 changed files with 1761 additions and 0 deletions

View File

@ -19,6 +19,7 @@ class CalendarAppConfig(AppConfig):
"""
import calendar_app.notification_signals # noqa: F401
from calendar_app import signals # noqa: F401 - старая система
import calendar_app.rules # django-rules: регистрация предикатов и правил доступа
try:
Employee = django_apps.get_model("employees", "Employee")

View File

@ -0,0 +1,376 @@
"""
django-rules: декларативные правила доступа для calendar_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_calendar_owner(user, calendar):
"""Пользователь является владельцем календаря"""
if calendar is None:
return False
# Для django-scheduler
if hasattr(calendar, 'creator'):
return calendar.creator == user
# Альтернативные поля
if hasattr(calendar, 'owner'):
return calendar.owner == user
return False
@rules.predicate
def is_personal_calendar(user, calendar):
"""Это личный календарь пользователя"""
if calendar is None:
return False
# Проверка slug для личного календаря
if hasattr(calendar, 'slug'):
return calendar.slug == f'personal-{user.pk}'
# Проверка через creator + тип календаря
if hasattr(calendar, 'calendar_type') and calendar.calendar_type == 'personal':
return is_calendar_owner(user, calendar)
return False
@rules.predicate
def has_calendar_access(user, calendar):
"""Пользователь имеет доступ к календарю через подписку или права"""
if calendar is None:
return False
# Публичный календарь
if hasattr(calendar, 'is_public') and calendar.is_public:
return True
# Календарь отдела
if hasattr(calendar, 'department') and hasattr(user, 'department'):
if calendar.department == user.department:
return True
# Подписка на календарь
if hasattr(calendar, 'subscriptions'):
return calendar.subscriptions.filter(user=user).exists()
# Календарь компании (доступен всем сотрудникам)
if hasattr(calendar, 'calendar_type') and calendar.calendar_type == 'company':
return user.is_staff
return False
@rules.predicate
def is_event_creator(user, event):
"""Пользователь является создателем события"""
if event is None:
return False
# django-scheduler использует creator_id
if hasattr(event, 'creator'):
return event.creator == user
if hasattr(event, 'created_by'):
return event.created_by == user
return False
@rules.predicate
def can_access_event_calendar(user, event):
"""Пользователь имеет доступ к календарю, где находится событие"""
if event is None or not hasattr(event, 'calendar'):
return False
return has_calendar_access(user, event.calendar) or is_calendar_owner(user, event.calendar)
@rules.predicate
def is_event_participant(user, event):
"""Пользователь является участником события"""
if event is None:
return False
# Проверка через участников
if hasattr(event, 'participants'):
return user in event.participants.all()
# Проверка через occurrence (для повторяющихся событий)
if hasattr(event, 'occurrence_set'):
return event.occurrence_set.filter(participants=user).exists()
return False
@rules.predicate
def can_manage_calendars(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_calendar(user, calendar):
"""Календарь относится к отделу пользователя"""
if calendar is None or not hasattr(user, 'department'):
return False
if hasattr(calendar, 'department'):
return calendar.department == user.department
return False
# -----------------------------------------------------------------------------
# ПРАВИЛА (rules)
# -----------------------------------------------------------------------------
# Просмотр календаря
rules.add_rule(
'calendar_app.view_calendar',
is_superuser | is_calendar_owner | is_personal_calendar |
has_calendar_access | is_department_calendar
)
# Изменение календаря (настройки, название, описание)
rules.add_rule(
'calendar_app.change_calendar',
is_superuser | is_calendar_owner | can_manage_calendars
)
# Удаление календаря
rules.add_rule(
'calendar_app.delete_calendar',
is_superuser | is_calendar_owner
)
# Создание события в календаре
rules.add_rule(
'calendar_app.create_event',
is_superuser | is_calendar_owner | has_calendar_access
)
# Просмотр события
rules.add_rule(
'calendar_app.view_event',
is_superuser | is_event_creator | can_access_event_calendar | is_event_participant
)
# Изменение события
rules.add_rule(
'calendar_app.change_event',
is_superuser | is_event_creator | is_calendar_owner
)
# Удаление события
rules.add_rule(
'calendar_app.delete_event',
is_superuser | is_event_creator | is_calendar_owner
)
# Подписка на календарь
rules.add_rule(
'calendar_app.subscribe_calendar',
rules.is_authenticated # Любой авторизованный может подписаться на публичный календарь
)
# Отмена подписки на календарь
rules.add_rule(
'calendar_app.unsubscribe_calendar',
rules.is_authenticated
)
# Экспорт календаря (iCal)
rules.add_rule(
'calendar_app.export_calendar',
is_superuser | is_calendar_owner | has_calendar_access
)
# Приглашение участников на событие
rules.add_rule(
'calendar_app.invite_participants',
is_superuser | is_event_creator | is_calendar_owner
)
# Просмотр всех календарей (для администраторов)
rules.add_rule(
'calendar_app.view_all_calendars',
is_superuser | can_manage_calendars
)
# -----------------------------------------------------------------------------
# ИСПОЛЬЗОВАНИЕ В КОДЕ
# -----------------------------------------------------------------------------
"""
# В views:
from django.core.exceptions import PermissionDenied
import rules
def calendar_detail(request, slug):
calendar = get_object_or_404(Calendar, slug=slug)
if not rules.test_rule('calendar_app.view_calendar', request.user, calendar):
raise PermissionDenied("У вас нет доступа к этому календарю")
events = calendar.event_set.all()
return render(request, 'calendar_app/calendar.html', {
'calendar': calendar,
'events': events
})
def event_create(request, calendar_slug):
calendar = get_object_or_404(Calendar, slug=calendar_slug)
if not rules.test_rule('calendar_app.create_event', request.user, calendar):
return JsonResponse({'error': 'Нет прав на создание событий'}, status=403)
# Логика создания события
event = Event.objects.create(
calendar=calendar,
creator=request.user,
title=request.POST.get('title'),
start=request.POST.get('start'),
end=request.POST.get('end'),
)
return JsonResponse({'event_id': event.pk})
def event_update(request, pk):
event = get_object_or_404(Event, pk=pk)
if not rules.test_rule('calendar_app.change_event', request.user, event):
return JsonResponse({'error': 'Нет прав на изменение'}, status=403)
event.title = request.POST.get('title', event.title)
event.save()
return JsonResponse({'success': True})
# В templates:
{% load rules %}
{% has_rule 'calendar_app.create_event' user calendar as can_create %}
{% if can_create %}
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#eventModal">
Создать событие
</button>
{% endif %}
{% for event in events %}
<div class="event">
<h4>{{ event.title }}</h4>
<p>{{ event.start|date:"d.m.Y H:i" }} - {{ event.end|date:"d.m.Y H:i" }}</p>
{% has_rule 'calendar_app.change_event' user event as can_edit %}
{% if can_edit %}
<a href="{% url 'calendar_app:event_edit' event.pk %}" class="btn btn-sm btn-primary">
Редактировать
</a>
{% endif %}
{% has_rule 'calendar_app.delete_event' user event as can_delete %}
{% if can_delete %}
<form method="post" action="{% url 'calendar_app:event_delete' event.pk %}"
onsubmit="return confirm('Удалить событие?')">
{% csrf_token %}
<button type="submit" class="btn btn-sm btn-danger">Удалить</button>
</form>
{% endif %}
</div>
{% endfor %}
# В DRF permissions (уже реализовано в api/v1/schedule/permissions.py):
from rest_framework import permissions
import rules
class IsCalendarOwnerOrAdmin(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
# Для Calendar
if hasattr(obj, 'creator'):
calendar = obj
# Для Event
elif hasattr(obj, 'calendar'):
calendar = obj.calendar
else:
return False
if request.method in permissions.SAFE_METHODS:
return rules.test_rule('calendar_app.view_calendar', request.user, calendar)
elif request.method in ['POST']:
return rules.test_rule('calendar_app.create_event', request.user, calendar)
elif request.method in ['PUT', 'PATCH']:
if hasattr(obj, 'calendar'): # Event
return rules.test_rule('calendar_app.change_event', request.user, obj)
else: # Calendar
return rules.test_rule('calendar_app.change_calendar', request.user, obj)
elif request.method == 'DELETE':
if hasattr(obj, 'calendar'):
return rules.test_rule('calendar_app.delete_event', request.user, obj)
else:
return rules.test_rule('calendar_app.delete_calendar', request.user, obj)
return False
# Фильтрация QuerySet (только доступные календари):
from django.db.models import Q
def get_accessible_calendars(user):
return Calendar.objects.filter(
Q(creator=user) | # Собственные календари
Q(slug=f'personal-{user.pk}') | # Личный календарь
Q(is_public=True) | # Публичные календари
Q(department=user.department) | # Календари отдела
Q(calendar_type='company') | # Корпоративный календарь
Q(subscriptions__user=user) # Подписки
).distinct()
# Middleware для защиты iCal экспорта:
class CalendarExportMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
if request.path.startswith('/calendar/export/'):
# Извлечение календаря
slug = request.path.split('/')[-2]
calendar = Calendar.objects.filter(slug=slug).first()
if calendar and not rules.test_rule('calendar_app.export_calendar', request.user, calendar):
return HttpResponseForbidden("У вас нет прав на экспорт этого календаря")
return self.get_response(request)
"""

View File

@ -9,6 +9,7 @@ class CommunicationsConfig(AppConfig):
def ready(self):
import communications.signals
import communications.notification_signals # Подключаем signals уведомлений
import communications.rules # django-rules: регистрация предикатов и правил доступа
def create_main_global_chat(sender, **kwargs):
from communications.models import Chat

View File

@ -0,0 +1,370 @@
"""
django-rules: декларативные правила доступа для communications (мессенджер)
Правила используются для проверки permissions на уровне объектов.
https://github.com/dfunckt/django-rules
"""
import rules
# -----------------------------------------------------------------------------
# ПРЕДИКАТЫ (predicates)
# -----------------------------------------------------------------------------
@rules.predicate
def is_superuser(user):
"""Суперпользователь имеет все права"""
return user.is_superuser
@rules.predicate
def is_chat_member(user, chat):
"""Пользователь является участником чата"""
if chat is None:
return False
# Проверка через members/participants
if hasattr(chat, 'members'):
return user in chat.members.all()
if hasattr(chat, 'participants'):
return user in chat.participants.all()
return False
@rules.predicate
def is_chat_owner(user, chat):
"""Пользователь является владельцем/создателем чата"""
if chat is None:
return False
if hasattr(chat, 'owner'):
return chat.owner == user
if hasattr(chat, 'created_by'):
return chat.created_by == user
return False
@rules.predicate
def is_chat_admin(user, chat):
"""Пользователь является администратором чата"""
if chat is None:
return False
# Проверка через admins
if hasattr(chat, 'admins'):
return user in chat.admins.all()
# Проверка через membership с ролью admin
if hasattr(chat, 'chatmembership_set'):
return chat.chatmembership_set.filter(
user=user, role__in=['admin', 'moderator']
).exists()
return False
@rules.predicate
def is_direct_chat_participant(user, chat):
"""Пользователь участвует в личном чате"""
if chat is None:
return False
# Проверка типа чата
chat_type = getattr(chat, 'chat_type', None) or getattr(chat, 'type', None)
if chat_type not in ['direct', 'private', 'personal']:
return False
return is_chat_member(user, chat)
@rules.predicate
def is_message_author(user, message):
"""Пользователь является автором сообщения"""
if message is None:
return False
return message.author == user or message.sender == user
@rules.predicate
def can_access_message_chat(user, message):
"""Пользователь имеет доступ к чату, где находится сообщение"""
if message is None or not hasattr(message, 'chat'):
return False
return is_chat_member(user, message.chat)
@rules.predicate
def is_public_chat(user, chat):
"""Чат является публичным (доступен всем)"""
if chat is None:
return False
return getattr(chat, 'is_public', False)
@rules.predicate
def is_department_chat(user, chat):
"""Чат отдела пользователя"""
if chat is None or not hasattr(user, 'department'):
return False
if hasattr(chat, 'department'):
return chat.department == user.department
return False
# -----------------------------------------------------------------------------
# ПРАВИЛА (rules)
# -----------------------------------------------------------------------------
# Просмотр чата (доступ к чату)
rules.add_rule(
'communications.view_chat',
is_superuser | is_chat_member | is_public_chat
)
# Отправка сообщений в чат
rules.add_rule(
'communications.send_message',
is_superuser | is_chat_member
)
# Изменение чата (название, описание, настройки)
rules.add_rule(
'communications.change_chat',
is_superuser | is_chat_owner | is_chat_admin
)
# Удаление чата
rules.add_rule(
'communications.delete_chat',
is_superuser | is_chat_owner
)
# Просмотр сообщения
rules.add_rule(
'communications.view_message',
is_superuser | can_access_message_chat
)
# Изменение сообщения (редактирование)
rules.add_rule(
'communications.change_message',
is_superuser | is_message_author
)
# Удаление сообщения
rules.add_rule(
'communications.delete_message',
is_superuser | is_message_author | is_chat_admin
)
# Добавление участников в чат
rules.add_rule(
'communications.add_members',
is_superuser | is_chat_owner | is_chat_admin
)
# Удаление участников из чата
rules.add_rule(
'communications.remove_members',
is_superuser | is_chat_owner | is_chat_admin
)
# Закрепление сообщений
rules.add_rule(
'communications.pin_message',
is_superuser | is_chat_owner | is_chat_admin
)
# Изменение ролей участников
rules.add_rule(
'communications.change_member_role',
is_superuser | is_chat_owner
)
# Выход из чата (покинуть чат)
rules.add_rule(
'communications.leave_chat',
is_chat_member & ~is_chat_owner # Все кроме владельца
)
# Просмотр истории чата
rules.add_rule(
'communications.view_chat_history',
is_superuser | is_chat_member
)
# Поиск по чатам/сообщениям
rules.add_rule(
'communications.search_messages',
rules.is_authenticated # Все авторизованные
)
# -----------------------------------------------------------------------------
# ИСПОЛЬЗОВАНИЕ В КОДЕ
# -----------------------------------------------------------------------------
"""
# В views:
from django.core.exceptions import PermissionDenied
import rules
def chat_detail(request, pk):
chat = get_object_or_404(Chat, pk=pk)
if not rules.test_rule('communications.view_chat', request.user, chat):
raise PermissionDenied("У вас нет доступа к этому чату")
messages = chat.messages.all()
return render(request, 'communications/chat.html', {
'chat': chat,
'messages': messages
})
def send_message(request, chat_pk):
chat = get_object_or_404(Chat, pk=chat_pk)
if not rules.test_rule('communications.send_message', request.user, chat):
return JsonResponse({'error': 'Нет прав на отправку сообщений'}, status=403)
message = Message.objects.create(
chat=chat,
author=request.user,
text=request.POST.get('text')
)
return JsonResponse({'message_id': message.pk})
def delete_message(request, pk):
message = get_object_or_404(Message, pk=pk)
if not rules.test_rule('communications.delete_message', request.user, message):
return JsonResponse({'error': 'Нет прав на удаление'}, status=403)
message.delete()
return JsonResponse({'success': True})
# В templates:
{% load rules %}
{% has_rule 'communications.send_message' user chat as can_send %}
{% if can_send %}
<form method="post" action="{% url 'communications:send' chat.pk %}" id="message-form">
{% csrf_token %}
<textarea name="text" class="form-control" placeholder="Введите сообщение..."></textarea>
<button type="submit" class="btn btn-primary">Отправить</button>
</form>
{% else %}
<p class="text-muted">Вы не можете отправлять сообщения в этот чат</p>
{% endif %}
{% for message in messages %}
<div class="message">
<strong>{{ message.author }}</strong>: {{ message.text }}
{% has_rule 'communications.delete_message' user message as can_delete %}
{% if can_delete %}
<button class="btn btn-sm btn-danger" onclick="deleteMessage({{ message.pk }})">
Удалить
</button>
{% endif %}
</div>
{% endfor %}
# В DRF permissions:
from rest_framework import permissions
import rules
class ChatPermission(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
if request.method in permissions.SAFE_METHODS:
return rules.test_rule('communications.view_chat', request.user, obj)
elif request.method in ['PUT', 'PATCH']:
return rules.test_rule('communications.change_chat', request.user, obj)
elif request.method == 'DELETE':
return rules.test_rule('communications.delete_chat', request.user, obj)
return False
class MessagePermission(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
if request.method in permissions.SAFE_METHODS:
return rules.test_rule('communications.view_message', request.user, obj)
elif request.method in ['PUT', 'PATCH']:
return rules.test_rule('communications.change_message', request.user, obj)
elif request.method == 'DELETE':
return rules.test_rule('communications.delete_message', request.user, obj)
return False
# В WebSocket consumer:
from channels.generic.websocket import AsyncJsonWebsocketConsumer
import rules
class ChatConsumer(AsyncJsonWebsocketConsumer):
async def connect(self):
chat_id = self.scope['url_route']['kwargs']['chat_id']
chat = await database_sync_to_async(Chat.objects.get)(pk=chat_id)
# Проверка доступа
if not await database_sync_to_async(rules.test_rule)(
'communications.view_chat', self.scope['user'], chat
):
await self.close()
return
await self.accept()
# Добавляем в группу чата
await self.channel_layer.group_add(f'chat_{chat_id}', self.channel_name)
async def receive_json(self, content):
action = content.get('action')
if action == 'send_message':
chat = await database_sync_to_async(Chat.objects.get)(pk=self.chat_id)
# Проверка прав на отправку
if not await database_sync_to_async(rules.test_rule)(
'communications.send_message', self.scope['user'], chat
):
await self.send_json({'error': 'Permission denied'})
return
# Сохранение сообщения
message = await database_sync_to_async(Message.objects.create)(
chat=chat,
author=self.scope['user'],
text=content['text']
)
# Рассылка всем участникам
await self.channel_layer.group_send(
f'chat_{chat.pk}',
{'type': 'chat.message', 'message': message.to_dict()}
)
# Фильтрация QuerySet (только доступные чаты):
from django.db.models import Q
def get_accessible_chats(user):
return Chat.objects.filter(
Q(members=user) | # Участник чата
Q(is_public=True) | # Публичный чат
Q(department=user.department) # Чат отдела
).distinct()
"""

View File

@ -8,3 +8,4 @@ class FeedConfig(AppConfig):
def ready(self):
# Подключаем signals для уведомлений
import feed.notification_signals # noqa
import feed.rules # django-rules: регистрация предикатов и правил доступа

386
backend/feed/rules.py Normal file
View File

@ -0,0 +1,386 @@
"""
django-rules: декларативные правила доступа для feed (лента новостей)
Правила используются для проверки permissions на уровне объектов.
https://github.com/dfunckt/django-rules
"""
import rules
# -----------------------------------------------------------------------------
# ПРЕДИКАТЫ (predicates)
# -----------------------------------------------------------------------------
@rules.predicate
def is_superuser(user):
"""Суперпользователь имеет все права"""
return user.is_superuser
@rules.predicate
def is_post_author(user, post):
"""Пользователь является автором поста"""
if post is None:
return False
return post.author == user or post.created_by == user
@rules.predicate
def can_publish_posts(user):
"""
Пользователь может публиковать посты в ленте.
Адаптируйте под вашу логику (по должности, группе и т.д.)
"""
if not hasattr(user, 'position'):
return False
position_name = getattr(user.position, 'name', '').lower()
# Руководители, PR-менеджеры, секретари могут публиковать
return any(keyword in position_name for keyword in [
'руководитель', 'начальник', 'директор', 'секретарь',
'pr', 'маркетинг', 'коммуникац'
])
@rules.predicate
def can_moderate_posts(user):
"""Пользователь может модерировать посты (удалять чужие, редактировать)"""
return can_publish_posts(user) or is_superuser(user)
@rules.predicate
def is_public_post(user, post):
"""Пост является публичным (доступен всем)"""
if post is None:
return False
return getattr(post, 'is_public', True)
@rules.predicate
def is_department_post(user, post):
"""Пост относится к отделу пользователя"""
if post is None or not hasattr(user, 'department'):
return False
if hasattr(post, 'department'):
return post.department == user.department
if hasattr(post, 'target_departments'):
return user.department in post.target_departments.all()
return False
@rules.predicate
def is_comment_author(user, comment):
"""Пользователь является автором комментария"""
if comment is None:
return False
return comment.author == user or comment.created_by == user
@rules.predicate
def can_access_comment_post(user, comment):
"""Пользователь имеет доступ к посту, где находится комментарий"""
if comment is None or not hasattr(comment, 'post'):
return False
post = comment.post
return is_public_post(user, post) or is_department_post(user, post)
@rules.predicate
def is_post_pinned(user, post):
"""Пост закреплён (может видеть любой сотрудник)"""
if post is None:
return False
return getattr(post, 'is_pinned', False)
# -----------------------------------------------------------------------------
# ПРАВИЛА (rules)
# -----------------------------------------------------------------------------
# Просмотр поста
rules.add_rule(
'feed.view_post',
is_superuser | is_public_post | is_department_post | is_post_pinned |
is_post_author # Автор всегда видит свой пост, даже если он не опубликован
)
# Создание поста
rules.add_rule(
'feed.create_post',
is_superuser | can_publish_posts
)
# Изменение поста
rules.add_rule(
'feed.change_post',
is_superuser | is_post_author | can_moderate_posts
)
# Удаление поста
rules.add_rule(
'feed.delete_post',
is_superuser | is_post_author | can_moderate_posts
)
# Публикация поста (изменение статуса draft → published)
rules.add_rule(
'feed.publish_post',
is_superuser | (is_post_author & can_publish_posts) | can_moderate_posts
)
# Закрепление поста
rules.add_rule(
'feed.pin_post',
is_superuser | can_moderate_posts
)
# Добавление комментария к посту
rules.add_rule(
'feed.comment_post',
rules.is_authenticated # Любой авторизованный может комментировать
)
# Просмотр комментария
rules.add_rule(
'feed.view_comment',
is_superuser | can_access_comment_post
)
# Изменение комментария
rules.add_rule(
'feed.change_comment',
is_superuser | is_comment_author
)
# Удаление комментария
rules.add_rule(
'feed.delete_comment',
is_superuser | is_comment_author | can_moderate_posts
)
# Лайк/реакция на пост
rules.add_rule(
'feed.react_to_post',
rules.is_authenticated # Любой авторизованный может ставить реакции
)
# Просмотр черновиков (draft posts)
rules.add_rule(
'feed.view_drafts',
is_superuser | can_publish_posts
)
# Просмотр статистики по постам
rules.add_rule(
'feed.view_statistics',
is_superuser | can_publish_posts
)
# -----------------------------------------------------------------------------
# ИСПОЛЬЗОВАНИЕ В КОДЕ
# -----------------------------------------------------------------------------
"""
# В views:
from django.core.exceptions import PermissionDenied
import rules
def post_detail(request, pk):
post = get_object_or_404(Post, pk=pk)
if not rules.test_rule('feed.view_post', request.user, post):
raise PermissionDenied("У вас нет доступа к этому посту")
comments = post.comments.all()
return render(request, 'feed/post_detail.html', {
'post': post,
'comments': comments
})
def post_create(request):
if not rules.test_rule('feed.create_post', request.user, None):
raise PermissionDenied("У вас нет прав на создание постов")
if request.method == 'POST':
post = Post.objects.create(
author=request.user,
title=request.POST.get('title'),
content=request.POST.get('content'),
status='draft'
)
return redirect('feed:post_detail', pk=post.pk)
return render(request, 'feed/post_create.html')
def post_publish(request, pk):
post = get_object_or_404(Post, pk=pk)
if not rules.test_rule('feed.publish_post', request.user, post):
return JsonResponse({'error': 'Нет прав на публикацию'}, status=403)
post.status = 'published'
post.published_at = timezone.now()
post.save()
return JsonResponse({'success': True})
def add_comment(request, post_pk):
post = get_object_or_404(Post, pk=post_pk)
if not rules.test_rule('feed.comment_post', request.user, None):
return JsonResponse({'error': 'Нет прав на комментирование'}, status=403)
comment = Comment.objects.create(
post=post,
author=request.user,
text=request.POST.get('text')
)
return JsonResponse({'comment_id': comment.pk})
# В templates:
{% load rules %}
{# Список постов #}
{% for post in posts %}
{% has_rule 'feed.view_post' user post as can_view %}
{% if can_view %}
<div class="post card mb-3">
<div class="card-body">
<h3>{{ post.title }}</h3>
<p>{{ post.content|truncatewords:50 }}</p>
<p class="text-muted">Автор: {{ post.author }}, {{ post.created_at|date:"d.m.Y H:i" }}</p>
<a href="{% url 'feed:post_detail' post.pk %}" class="btn btn-primary">
Читать далее
</a>
{% has_rule 'feed.change_post' user post as can_edit %}
{% if can_edit %}
<a href="{% url 'feed:post_edit' post.pk %}" class="btn btn-secondary">
Редактировать
</a>
{% endif %}
{% has_rule 'feed.delete_post' user post as can_delete %}
{% if can_delete %}
<form method="post" action="{% url 'feed:post_delete' post.pk %}"
style="display:inline" onsubmit="return confirm('Удалить пост?')">
{% csrf_token %}
<button type="submit" class="btn btn-danger">Удалить</button>
</form>
{% endif %}
{% has_rule 'feed.pin_post' user post as can_pin %}
{% if can_pin and not post.is_pinned %}
<form method="post" action="{% url 'feed:post_pin' post.pk %}" style="display:inline">
{% csrf_token %}
<button type="submit" class="btn btn-warning">Закрепить</button>
</form>
{% endif %}
</div>
</div>
{% endif %}
{% endfor %}
{# Создание поста #}
{% has_rule 'feed.create_post' user None as can_create %}
{% if can_create %}
<a href="{% url 'feed:post_create' %}" class="btn btn-success mb-3">
Создать пост
</a>
{% endif %}
{# Комментарии #}
{% has_rule 'feed.comment_post' user None as can_comment %}
{% if can_comment %}
<form method="post" action="{% url 'feed:add_comment' post.pk %}">
{% csrf_token %}
<textarea name="text" class="form-control mb-2" placeholder="Ваш комментарий..."></textarea>
<button type="submit" class="btn btn-primary">Отправить</button>
</form>
{% 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()})
"""

View File

@ -9,3 +9,4 @@ class ProcurementConfig(AppConfig):
def ready(self):
"""Подключение сигналов при загрузке приложения."""
import procurement.signals # noqa: F401
import procurement.rules # django-rules: регистрация предикатов и правил доступа

View File

@ -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' %}
<form method="post" action="{% url 'procurement:approve' purchase_request.pk %}">
{% csrf_token %}
<button type="submit" class="btn btn-success">Согласовать</button>
</form>
<form method="post" action="{% url 'procurement:reject' purchase_request.pk %}">
{% csrf_token %}
<button type="submit" class="btn btn-danger">Отклонить</button>
</form>
{% endif %}
{% has_rule 'procurement.manage_supplier' user None as can_manage %}
{% if can_manage %}
<a href="{% url 'procurement:supplier_create' %}" class="btn btn-primary">
Добавить поставщика
</a>
{% 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()
"""

View File

@ -8,3 +8,4 @@ class RequestsAppConfig(AppConfig):
def ready(self):
# Подключаем signals для уведомлений
import requests_app.notification_signals # noqa
import requests_app.rules # django-rules: регистрация предикатов и правил доступа

View File

@ -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 %}
<form method="post" action="{% url 'requests_app:change_status' request.pk %}">
{% csrf_token %}
<select name="status" class="form-select">
<option value="new">Новая</option>
<option value="in_progress">В работе</option>
<option value="completed">Выполнено</option>
</select>
<button type="submit" class="btn btn-primary">Изменить статус</button>
</form>
{% 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()
"""