Harden backend warnings and lint policy
This commit is contained in:
parent
3d4416615e
commit
9edb44502a
14
backend/.flake8
Normal file
14
backend/.flake8
Normal file
@ -0,0 +1,14 @@
|
||||
[flake8]
|
||||
max-line-length = 88
|
||||
exclude =
|
||||
.git,
|
||||
.venv,
|
||||
__pycache__,
|
||||
node_modules,
|
||||
staticfiles,
|
||||
media,
|
||||
migrations
|
||||
extend-ignore =
|
||||
W503
|
||||
per-file-ignores =
|
||||
communications/api/viewsets.py:E501
|
||||
@ -568,9 +568,9 @@ class ScheduleEventRelationViewSet(viewsets.ModelViewSet):
|
||||
def get_queryset(self):
|
||||
"""Фильтрация по событию."""
|
||||
qs = super().get_queryset()
|
||||
|
||||
|
||||
event_id = self.request.query_params.get('event')
|
||||
if event_id:
|
||||
qs = qs.filter(event_id=event_id)
|
||||
|
||||
return qs
|
||||
|
||||
return qs.order_by('id')
|
||||
|
||||
@ -19,16 +19,9 @@ from datetime import timezone as dt_tz
|
||||
|
||||
from asgiref.sync import async_to_sync
|
||||
from channels.layers import get_channel_layer
|
||||
from ..models import (Chat, ChatMembership, ChatReadState,
|
||||
ChatUserSettings, Message,
|
||||
MessageAttachment, MessageReaction, Poll,
|
||||
PollOption, PollVote)
|
||||
from communications.serialization import serialize_message
|
||||
from ..utils import _coerce_ts, user_can_access_chat
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.db import transaction
|
||||
from django.db.models import Count, Exists, OuterRef, Prefetch, Q, Subquery
|
||||
from django.db.models.functions import Coalesce
|
||||
from django.db.models import Prefetch, Q
|
||||
from django.utils import timezone
|
||||
from rest_framework import status, viewsets
|
||||
from rest_framework.decorators import action
|
||||
@ -36,13 +29,32 @@ from rest_framework.parsers import FormParser, JSONParser, MultiPartParser
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
|
||||
from .serializers import (BulkDeleteSerializer, ChatDetailSerializer,
|
||||
ChatListSerializer, ForwardMessageSerializer,
|
||||
MessageAttachmentSerializer, MessageCreateSerializer,
|
||||
MessageDetailSerializer, MessageEditSerializer,
|
||||
MessageListSerializer, PollSerializer,
|
||||
ReactionSerializer)
|
||||
from .permissions import ChatPermission, MessagePermission
|
||||
from .serializers import (
|
||||
BulkDeleteSerializer,
|
||||
ChatDetailSerializer,
|
||||
ChatListSerializer,
|
||||
ForwardMessageSerializer,
|
||||
MessageCreateSerializer,
|
||||
MessageDetailSerializer,
|
||||
MessageEditSerializer,
|
||||
MessageListSerializer,
|
||||
PollSerializer,
|
||||
ReactionSerializer,
|
||||
)
|
||||
from ..models import (
|
||||
Chat,
|
||||
ChatMembership,
|
||||
ChatReadState,
|
||||
ChatUserSettings,
|
||||
Message,
|
||||
MessageAttachment,
|
||||
MessageReaction,
|
||||
Poll,
|
||||
PollVote,
|
||||
)
|
||||
from ..utils import _coerce_ts, user_can_access_chat
|
||||
from communications.serialization import serialize_message
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
User = get_user_model()
|
||||
@ -80,15 +92,14 @@ class ChatViewSet(viewsets.ModelViewSet):
|
||||
def get_queryset(self):
|
||||
"""
|
||||
Чаты доступные пользователю с аннотацией непрочитанных.
|
||||
|
||||
|
||||
ОПТИМИЗИРОВАНО: Используем денормализованное поле unread_count из ChatReadState
|
||||
вместо подзапросов COUNT(*). Это убирает N+1 проблему и ускоряет в ~100x.
|
||||
|
||||
|
||||
ФИЛЬТРАЦИЯ: Чаты типа 'comments' исключены из списка (list action),
|
||||
но доступны по прямой ссылке (retrieve action).
|
||||
"""
|
||||
user = self.request.user
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
|
||||
queryset = Chat.objects.filter(
|
||||
Q(memberships__user=user, memberships__is_active=True)
|
||||
@ -117,12 +128,12 @@ class ChatViewSet(viewsets.ModelViewSet):
|
||||
to_attr='my_read_state'
|
||||
)
|
||||
).distinct()
|
||||
|
||||
|
||||
# Исключаем чаты-комментарии из общего списка
|
||||
# Они доступны только через прямой запрос (retrieve) или через контекст поста
|
||||
if self.action == 'list':
|
||||
queryset = queryset.exclude(type='comments')
|
||||
|
||||
|
||||
return queryset.order_by('-created_at')
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
@ -211,29 +222,29 @@ class ChatViewSet(viewsets.ModelViewSet):
|
||||
def leave(self, request, pk=None):
|
||||
"""Покинуть чат (выход пользователя из чата)"""
|
||||
import rules
|
||||
|
||||
|
||||
chat = self.get_object()
|
||||
|
||||
|
||||
# Проверка прав через django-rules
|
||||
if not rules.test_rule('communications.leave_chat', request.user, chat):
|
||||
return Response(
|
||||
{'error': 'Permission denied'},
|
||||
status=status.HTTP_403_FORBIDDEN
|
||||
)
|
||||
|
||||
|
||||
# Нельзя покинуть чат, если ты его владелец
|
||||
if chat.created_by == request.user:
|
||||
return Response(
|
||||
{'error': 'Chat owner cannot leave the chat'},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
|
||||
# MIGRATION: Убрали participants.remove, только деактивируем membership
|
||||
membership = ChatMembership.objects.filter(
|
||||
chat=chat,
|
||||
user=request.user
|
||||
).first()
|
||||
|
||||
|
||||
if membership:
|
||||
membership.is_active = False
|
||||
membership.left_at = timezone.now()
|
||||
@ -244,7 +255,7 @@ class ChatViewSet(viewsets.ModelViewSet):
|
||||
{'error': 'You are not a member of this chat'},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
|
||||
return Response({
|
||||
'ok': True,
|
||||
'message': 'Successfully left the chat'
|
||||
@ -254,23 +265,23 @@ class ChatViewSet(viewsets.ModelViewSet):
|
||||
def add_member(self, request, pk=None):
|
||||
"""Добавить участника в чат"""
|
||||
import rules
|
||||
|
||||
|
||||
chat = self.get_object()
|
||||
user_id = request.data.get('user_id')
|
||||
|
||||
|
||||
if not user_id:
|
||||
return Response(
|
||||
{'error': 'user_id is required'},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
|
||||
# Проверка прав
|
||||
if not rules.test_rule('communications.add_members', request.user, chat):
|
||||
return Response(
|
||||
{'error': 'Permission denied'},
|
||||
status=status.HTTP_403_FORBIDDEN
|
||||
)
|
||||
|
||||
|
||||
try:
|
||||
user_to_add = User.objects.get(pk=user_id)
|
||||
except User.DoesNotExist:
|
||||
@ -278,7 +289,7 @@ class ChatViewSet(viewsets.ModelViewSet):
|
||||
{'error': 'User not found'},
|
||||
status=status.HTTP_404_NOT_FOUND
|
||||
)
|
||||
|
||||
|
||||
# MIGRATION: Проверяем через memberships вместо participants
|
||||
if ChatMembership.objects.filter(
|
||||
chat=chat,
|
||||
@ -289,7 +300,7 @@ class ChatViewSet(viewsets.ModelViewSet):
|
||||
{'error': 'User is already a member'},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
|
||||
# Создаем или восстанавливаем membership для чатов с управлением участниками
|
||||
if chat.type in ['group', 'channel', 'announcement']:
|
||||
membership, created = ChatMembership.objects.get_or_create(
|
||||
@ -301,7 +312,7 @@ class ChatViewSet(viewsets.ModelViewSet):
|
||||
'is_active': True
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
# Если membership уже существовал (например, пользователь раньше покинул чат),
|
||||
# восстанавливаем его активность
|
||||
if not created and not membership.is_active:
|
||||
@ -310,7 +321,7 @@ class ChatViewSet(viewsets.ModelViewSet):
|
||||
membership.invited_by = request.user # Обновляем кто пригласил повторно
|
||||
membership.save()
|
||||
chat.participants.add(user_to_add)
|
||||
|
||||
|
||||
return Response({
|
||||
'ok': True,
|
||||
'message': 'User added successfully'
|
||||
@ -320,23 +331,23 @@ class ChatViewSet(viewsets.ModelViewSet):
|
||||
def remove_member(self, request, pk=None):
|
||||
"""Удалить участника из чата"""
|
||||
import rules
|
||||
|
||||
|
||||
chat = self.get_object()
|
||||
user_id = request.data.get('user_id')
|
||||
|
||||
|
||||
if not user_id:
|
||||
return Response(
|
||||
{'error': 'user_id is required'},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
|
||||
# Проверка прав
|
||||
if not rules.test_rule('communications.remove_members', request.user, chat):
|
||||
return Response(
|
||||
{'error': 'Permission denied'},
|
||||
status=status.HTTP_403_FORBIDDEN
|
||||
)
|
||||
|
||||
|
||||
try:
|
||||
user_to_remove = User.objects.get(pk=user_id)
|
||||
except User.DoesNotExist:
|
||||
@ -344,31 +355,31 @@ class ChatViewSet(viewsets.ModelViewSet):
|
||||
{'error': 'User not found'},
|
||||
status=status.HTTP_404_NOT_FOUND
|
||||
)
|
||||
|
||||
|
||||
# Нельзя удалить владельца
|
||||
if chat.created_by == user_to_remove:
|
||||
return Response(
|
||||
{'error': 'Cannot remove chat owner'},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
|
||||
# MIGRATION: Убрали participants.remove, только деактивируем membership
|
||||
membership = ChatMembership.objects.filter(
|
||||
chat=chat,
|
||||
user=user_to_remove
|
||||
).first()
|
||||
|
||||
|
||||
if not membership:
|
||||
return Response(
|
||||
{'error': 'User is not a member'},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
|
||||
membership.is_active = False
|
||||
membership.left_at = timezone.now()
|
||||
membership.save(update_fields=['is_active', 'left_at'])
|
||||
chat.participants.remove(user_to_remove)
|
||||
|
||||
|
||||
return Response({
|
||||
'ok': True,
|
||||
'message': 'User removed successfully'
|
||||
@ -378,17 +389,17 @@ class ChatViewSet(viewsets.ModelViewSet):
|
||||
def change_role(self, request, pk=None):
|
||||
"""Изменить роль участника чата"""
|
||||
import rules
|
||||
|
||||
|
||||
chat = self.get_object()
|
||||
user_id = request.data.get('user_id')
|
||||
new_role = request.data.get('role')
|
||||
|
||||
|
||||
if not user_id or not new_role:
|
||||
return Response(
|
||||
{'error': 'user_id and role are required'},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
|
||||
# Проверка валидности роли
|
||||
valid_roles = ['admin', 'moderator', 'member', 'guest']
|
||||
if new_role not in valid_roles:
|
||||
@ -396,10 +407,10 @@ class ChatViewSet(viewsets.ModelViewSet):
|
||||
{'error': f'Invalid role. Must be one of: {", ".join(valid_roles)}'},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
|
||||
# Проверка прав (только владелец может менять роли)
|
||||
can_change = rules.test_rule('communications.change_member_role', request.user, chat)
|
||||
|
||||
|
||||
# Добавляем логирование для отладки
|
||||
logger.warning(
|
||||
f"[change_role] User {request.user.id} trying to change role in chat {chat.id}. "
|
||||
@ -407,13 +418,13 @@ class ChatViewSet(viewsets.ModelViewSet):
|
||||
f"request.user.id={request.user.id}, "
|
||||
f"can_change={can_change}"
|
||||
)
|
||||
|
||||
|
||||
if not can_change:
|
||||
return Response(
|
||||
{'error': 'Permission denied. Only chat owner can change roles.'},
|
||||
status=status.HTTP_403_FORBIDDEN
|
||||
)
|
||||
|
||||
|
||||
try:
|
||||
user_to_change = User.objects.get(pk=user_id)
|
||||
except User.DoesNotExist:
|
||||
@ -421,14 +432,14 @@ class ChatViewSet(viewsets.ModelViewSet):
|
||||
{'error': 'User not found'},
|
||||
status=status.HTTP_404_NOT_FOUND
|
||||
)
|
||||
|
||||
|
||||
# Нельзя изменить роль владельца
|
||||
if chat.created_by == user_to_change:
|
||||
return Response(
|
||||
{'error': 'Cannot change role of chat owner'},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
|
||||
# Получаем или создаем membership
|
||||
membership, created = ChatMembership.objects.get_or_create(
|
||||
chat=chat,
|
||||
@ -439,13 +450,13 @@ class ChatViewSet(viewsets.ModelViewSet):
|
||||
'is_active': True # Явно устанавливаем is_active при создании
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
logger.warning(
|
||||
f"[change_role] membership found: created={created}, "
|
||||
f"user_id={user_to_change.id}, old_role={membership.role}, new_role={new_role}, "
|
||||
f"is_active={membership.is_active}"
|
||||
)
|
||||
|
||||
|
||||
if not created:
|
||||
# Обновляем существующий membership
|
||||
old_role = membership.role
|
||||
@ -455,7 +466,7 @@ class ChatViewSet(viewsets.ModelViewSet):
|
||||
membership.left_at = None # Сбрасываем left_at если был
|
||||
membership.set_permissions_for_role()
|
||||
membership.save()
|
||||
|
||||
|
||||
# Перезагружаем из БД чтобы убедиться что сохранилось
|
||||
membership.refresh_from_db()
|
||||
logger.warning(
|
||||
@ -464,7 +475,7 @@ class ChatViewSet(viewsets.ModelViewSet):
|
||||
f"old_is_active={old_is_active}, new_is_active={membership.is_active}, "
|
||||
f"saved_correctly={membership.role == new_role and membership.is_active}"
|
||||
)
|
||||
|
||||
|
||||
return Response({
|
||||
'ok': True,
|
||||
'message': f'User role changed to {new_role}',
|
||||
@ -897,13 +908,13 @@ class MessageViewSet(viewsets.ModelViewSet):
|
||||
def get_queryset(self):
|
||||
"""
|
||||
Сообщения доступные пользователю
|
||||
|
||||
|
||||
Включает сообщения из чатов:
|
||||
- где user в participants (прямое участие)
|
||||
- где user в ChatMembership (роли)
|
||||
- где include_all_users=True (открытые чаты)
|
||||
- context-based чаты (type=comments с context_object)
|
||||
|
||||
|
||||
Детальная проверка доступа выполняется через MessagePermission
|
||||
"""
|
||||
user = self.request.user
|
||||
@ -915,13 +926,13 @@ class MessageViewSet(viewsets.ModelViewSet):
|
||||
| Q(participants=user)
|
||||
| Q(include_all_users=True)
|
||||
).distinct()
|
||||
|
||||
|
||||
# Context-based чаты (комментарии к постам и т.д.)
|
||||
# Проверка доступа к context_object будет в permissions
|
||||
context_based_chats = Chat.objects.filter(
|
||||
Q(type='comments') & ~Q(context_object_id=None)
|
||||
).distinct()
|
||||
|
||||
|
||||
# Объединяем оба набора
|
||||
all_chats = accessible_chats | context_based_chats
|
||||
|
||||
|
||||
@ -22,7 +22,6 @@ def test_ldap_connection():
|
||||
print(f"\n✓ LDAP подключение успешно")
|
||||
print(f"✓ Сервер: {conn.server}")
|
||||
print(f"✓ Пользователь: {conn.user}")
|
||||
return True
|
||||
except Exception as e:
|
||||
pytest.fail(f"Не удалось подключиться к LDAP: {e}")
|
||||
|
||||
|
||||
@ -6,11 +6,14 @@ API endpoints для системы уведомлений v2.0
|
||||
- verb вместо category/type
|
||||
- UserChannelPreferences для настроек каналов
|
||||
"""
|
||||
from datetime import datetime
|
||||
|
||||
from drf_spectacular.utils import OpenApiParameter, extend_schema
|
||||
from django.db.models import Count, Q
|
||||
from rest_framework import status
|
||||
from rest_framework.decorators import api_view, permission_classes
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import status
|
||||
|
||||
from ..models import Notification, UserChannelPreferences
|
||||
from .serializers import (
|
||||
@ -33,11 +36,23 @@ def _get_notification_title(notification):
|
||||
# Приоритет: title из data (задается при создании уведомления)
|
||||
if notification.data and 'title' in notification.data:
|
||||
return notification.data['title']
|
||||
|
||||
|
||||
# Fallback: генерируем простой заголовок из verb
|
||||
return notification.verb.replace('_', ' ').title()
|
||||
|
||||
|
||||
def _format_time_or_none(value):
|
||||
if value is None:
|
||||
return None
|
||||
return value.strftime('%H:%M')
|
||||
|
||||
|
||||
def _parse_time_or_none(value):
|
||||
if not value:
|
||||
return None
|
||||
return datetime.strptime(value, '%H:%M').time()
|
||||
|
||||
|
||||
@extend_schema(
|
||||
tags=["Notifications"],
|
||||
summary="Получить список уведомлений",
|
||||
@ -54,8 +69,8 @@ def _get_notification_title(notification):
|
||||
@permission_classes([IsAuthenticated])
|
||||
def get_notifications(request):
|
||||
"""
|
||||
Получить список уведомлений с пагинацией и фильтрами
|
||||
|
||||
Получить список уведомлений с пагинацией и фильтрами.
|
||||
|
||||
Query params:
|
||||
- page: номер страницы (default: 1)
|
||||
- page_size: размер страницы (default: 20)
|
||||
@ -84,10 +99,10 @@ def get_notifications(request):
|
||||
# Фильтры
|
||||
if verb:
|
||||
queryset = queryset.filter(verb=verb)
|
||||
|
||||
|
||||
if unread_only:
|
||||
queryset = queryset.unread()
|
||||
|
||||
|
||||
if search:
|
||||
queryset = queryset.filter(description__icontains=search)
|
||||
|
||||
@ -110,8 +125,8 @@ def get_notifications(request):
|
||||
'title': _get_notification_title(n),
|
||||
'message': n.description,
|
||||
'short_message': (
|
||||
n.description[:100] + '...'
|
||||
if len(n.description) > 100
|
||||
n.description[:100] + '...'
|
||||
if len(n.description) > 100
|
||||
else n.description
|
||||
),
|
||||
'is_read': not n.unread,
|
||||
@ -174,12 +189,12 @@ def mark_as_read(request, notification_id):
|
||||
recipient=request.user
|
||||
)
|
||||
notification.mark_as_read()
|
||||
|
||||
|
||||
return Response({
|
||||
'status': 'success',
|
||||
'notification_id': notification_id
|
||||
})
|
||||
|
||||
|
||||
except Notification.DoesNotExist:
|
||||
return Response(
|
||||
{'status': 'error', 'message': 'Notification not found'},
|
||||
@ -203,12 +218,12 @@ def mark_as_unread(request, notification_id):
|
||||
recipient=request.user
|
||||
)
|
||||
notification.mark_as_unread()
|
||||
|
||||
|
||||
return Response({
|
||||
'status': 'success',
|
||||
'notification_id': notification_id
|
||||
})
|
||||
|
||||
|
||||
except Notification.DoesNotExist:
|
||||
return Response(
|
||||
{'status': 'error', 'message': 'Notification not found'},
|
||||
@ -227,16 +242,16 @@ def mark_as_unread(request, notification_id):
|
||||
def mark_all_as_read(request):
|
||||
"""Отметить все уведомления как прочитанные"""
|
||||
verb = request.data.get('verb') # Опционально - только определенный тип
|
||||
|
||||
|
||||
queryset = Notification.objects.filter(
|
||||
recipient=request.user,
|
||||
unread=True,
|
||||
deleted=False,
|
||||
)
|
||||
|
||||
|
||||
if verb:
|
||||
queryset = queryset.filter(verb=verb)
|
||||
|
||||
|
||||
count = queryset.mark_all_as_read()
|
||||
|
||||
return Response({
|
||||
@ -309,9 +324,9 @@ def delete_notification(request, notification_id):
|
||||
)
|
||||
notification.deleted = True
|
||||
notification.save(update_fields=['deleted'])
|
||||
|
||||
|
||||
return Response({'status': 'success'})
|
||||
|
||||
|
||||
except Notification.DoesNotExist:
|
||||
return Response(
|
||||
{'status': 'error', 'message': 'Notification not found'},
|
||||
@ -349,12 +364,10 @@ def delete_all_read(request):
|
||||
@permission_classes([IsAuthenticated])
|
||||
def get_verb_types(request):
|
||||
"""
|
||||
Получить список типов (verb) уведомлений с количеством
|
||||
|
||||
Получить список типов (verb) уведомлений с количеством.
|
||||
|
||||
Возвращает статистику по типам уведомлений пользователя.
|
||||
"""
|
||||
from django.db.models import Count, Q
|
||||
|
||||
verb_stats = Notification.objects.filter(
|
||||
recipient=request.user,
|
||||
deleted=False,
|
||||
@ -394,11 +407,11 @@ def get_verb_types(request):
|
||||
@permission_classes([IsAuthenticated])
|
||||
def channel_preferences(request):
|
||||
"""
|
||||
Получить или обновить настройки каналов уведомлений
|
||||
|
||||
Получить или обновить настройки каналов уведомлений.
|
||||
|
||||
GET: Возвращает текущие настройки
|
||||
PUT: Обновляет настройки
|
||||
|
||||
|
||||
Настройки:
|
||||
- web_enabled: WebSocket уведомления (bool)
|
||||
- email_enabled: Email уведомления (bool)
|
||||
@ -410,10 +423,10 @@ def channel_preferences(request):
|
||||
- disabled_verbs: Список отключенных типов (array)
|
||||
"""
|
||||
# Получаем или создаем настройки
|
||||
prefs, created = UserChannelPreferences.objects.get_or_create(
|
||||
prefs, _created = UserChannelPreferences.objects.get_or_create(
|
||||
user=request.user
|
||||
)
|
||||
|
||||
|
||||
if request.method == 'GET':
|
||||
data = {
|
||||
'web_enabled': prefs.web_enabled,
|
||||
@ -421,72 +434,76 @@ def channel_preferences(request):
|
||||
'email_frequency': prefs.email_frequency,
|
||||
'push_enabled': prefs.push_enabled,
|
||||
'dnd_enabled': prefs.dnd_enabled,
|
||||
'dnd_start_time': prefs.dnd_start_time.strftime('%H:%M') if prefs.dnd_start_time else None,
|
||||
'dnd_end_time': prefs.dnd_end_time.strftime('%H:%M') if prefs.dnd_end_time else None,
|
||||
'dnd_start_time': _format_time_or_none(prefs.dnd_start_time),
|
||||
'dnd_end_time': _format_time_or_none(prefs.dnd_end_time),
|
||||
'disabled_verbs': prefs.disabled_verbs,
|
||||
}
|
||||
return Response(data)
|
||||
|
||||
elif request.method == 'PUT':
|
||||
|
||||
if request.method == 'PUT':
|
||||
# Обновляем настройки
|
||||
if 'web_enabled' in request.data:
|
||||
prefs.web_enabled = request.data.get('web_enabled')
|
||||
|
||||
|
||||
if 'email_enabled' in request.data:
|
||||
prefs.email_enabled = request.data.get('email_enabled')
|
||||
|
||||
|
||||
if 'email_frequency' in request.data:
|
||||
frequency = request.data.get('email_frequency')
|
||||
if frequency in ['instant', 'daily', 'weekly', 'disabled']:
|
||||
prefs.email_frequency = frequency
|
||||
|
||||
|
||||
if 'push_enabled' in request.data:
|
||||
prefs.push_enabled = request.data.get('push_enabled')
|
||||
|
||||
|
||||
if 'dnd_enabled' in request.data:
|
||||
prefs.dnd_enabled = request.data.get('dnd_enabled')
|
||||
|
||||
|
||||
if 'dnd_start_time' in request.data:
|
||||
from datetime import datetime
|
||||
time_str = request.data.get('dnd_start_time')
|
||||
if time_str:
|
||||
prefs.dnd_start_time = datetime.strptime(time_str, '%H:%M').time()
|
||||
|
||||
prefs.dnd_start_time = _parse_time_or_none(time_str)
|
||||
|
||||
if 'dnd_end_time' in request.data:
|
||||
from datetime import datetime
|
||||
time_str = request.data.get('dnd_end_time')
|
||||
if time_str:
|
||||
prefs.dnd_end_time = datetime.strptime(time_str, '%H:%M').time()
|
||||
|
||||
prefs.dnd_end_time = _parse_time_or_none(time_str)
|
||||
|
||||
if 'disabled_verbs' in request.data:
|
||||
prefs.disabled_verbs = request.data.get('disabled_verbs')
|
||||
|
||||
|
||||
prefs.save()
|
||||
|
||||
|
||||
return Response({
|
||||
'status': 'success',
|
||||
'message': 'Preferences updated'
|
||||
})
|
||||
|
||||
return Response(
|
||||
{'status': 'error', 'message': 'Method not allowed'},
|
||||
status=status.HTTP_405_METHOD_NOT_ALLOWED,
|
||||
)
|
||||
|
||||
|
||||
@extend_schema(
|
||||
tags=["Notifications"],
|
||||
summary="Получить публичный VAPID ключ",
|
||||
responses={200: VapidPublicKeyResponseSerializer, 503: VapidPublicKeyResponseSerializer},
|
||||
responses={
|
||||
200: VapidPublicKeyResponseSerializer,
|
||||
503: VapidPublicKeyResponseSerializer,
|
||||
},
|
||||
)
|
||||
@api_view(['GET'])
|
||||
@permission_classes([IsAuthenticated])
|
||||
def get_vapid_public_key(request):
|
||||
"""
|
||||
Получить публичный VAPID ключ для Web Push подписки
|
||||
|
||||
Получить публичный VAPID ключ для Web Push подписки.
|
||||
|
||||
Returns:
|
||||
- vapid_public_key: публичный ключ в формате base64url
|
||||
"""
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
vapid_key = getattr(settings, 'VAPID_PUBLIC_KEY', None)
|
||||
|
||||
|
||||
if not vapid_key:
|
||||
return Response(
|
||||
{
|
||||
@ -496,7 +513,7 @@ def get_vapid_public_key(request):
|
||||
},
|
||||
status=status.HTTP_503_SERVICE_UNAVAILABLE
|
||||
)
|
||||
|
||||
|
||||
return Response({
|
||||
'status': 'success',
|
||||
'vapid_public_key': vapid_key
|
||||
@ -513,25 +530,25 @@ def get_vapid_public_key(request):
|
||||
@permission_classes([IsAuthenticated])
|
||||
def subscribe_push(request):
|
||||
"""
|
||||
Подписка на Web Push уведомления
|
||||
|
||||
Подписка на Web Push уведомления.
|
||||
|
||||
Body:
|
||||
- endpoint: URL для отправки push
|
||||
- keys: {p256dh, auth} ключи шифрования
|
||||
- device_name: название устройства/браузера (опционально)
|
||||
"""
|
||||
from push_notifications.models import WebPushDevice
|
||||
|
||||
|
||||
endpoint = request.data.get('endpoint')
|
||||
keys = request.data.get('keys', {})
|
||||
device_name = request.data.get('device_name', 'unknown')
|
||||
|
||||
|
||||
if not endpoint or not keys:
|
||||
return Response(
|
||||
{'status': 'error', 'message': 'endpoint and keys required'},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
|
||||
# Регистрируем устройство
|
||||
device, created = WebPushDevice.objects.update_or_create(
|
||||
user=request.user,
|
||||
@ -543,10 +560,14 @@ def subscribe_push(request):
|
||||
'active': True,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
return Response({
|
||||
'status': 'success',
|
||||
'message': 'Push subscription created' if created else 'Push subscription updated',
|
||||
'message': (
|
||||
'Push subscription created'
|
||||
if created
|
||||
else 'Push subscription updated'
|
||||
),
|
||||
'created': created,
|
||||
'device_id': device.id
|
||||
})
|
||||
@ -564,9 +585,9 @@ def subscribe_push(request):
|
||||
def unsubscribe_push(request):
|
||||
"""Отписка от Web Push уведомлений"""
|
||||
from push_notifications.models import WebPushDevice
|
||||
|
||||
|
||||
endpoint = request.data.get('endpoint')
|
||||
|
||||
|
||||
if endpoint:
|
||||
deleted_count, _ = WebPushDevice.objects.filter(
|
||||
user=request.user,
|
||||
@ -577,7 +598,7 @@ def unsubscribe_push(request):
|
||||
deleted_count, _ = WebPushDevice.objects.filter(
|
||||
user=request.user
|
||||
).delete()
|
||||
|
||||
|
||||
return Response({
|
||||
'status': 'success',
|
||||
'message': f'Deleted {deleted_count} push subscription(s)',
|
||||
|
||||
@ -25,3 +25,9 @@ norecursedirs = .git .venv __pycache__ node_modules staticfiles media scripts te
|
||||
|
||||
# Опции для вывода
|
||||
console_output_style = progress
|
||||
|
||||
filterwarnings =
|
||||
ignore:tagMap is deprecated\. Please use TAG_MAP instead\.:DeprecationWarning:pyasn1\.codec\.ber\.encoder
|
||||
ignore:typeMap is deprecated\. Please use TYPE_MAP instead\.:DeprecationWarning:pyasn1\.codec\.ber\.encoder
|
||||
ignore:datetime\.datetime\.utcfromtimestamp\(\) is deprecated:DeprecationWarning:ldapdb\.models\.fields
|
||||
ignore:The 'django-fsm' package has been integrated into 'viewflow' as 'viewflow\.fsm'.*:UserWarning:django_fsm
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user