You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
488 lines
17 KiB
488 lines
17 KiB
import math
|
|
import re
|
|
from django.contrib.auth import get_user_model
|
|
from django.db import transaction
|
|
from django.db.models import Q, Sum
|
|
from django.shortcuts import get_object_or_404
|
|
from rest_framework import status
|
|
from rest_framework.authentication import TokenAuthentication
|
|
from rest_framework.authtoken.models import Token
|
|
from rest_framework.permissions import BasePermission, IsAuthenticated
|
|
from rest_framework.response import Response
|
|
from rest_framework.views import APIView
|
|
|
|
from apps.support.models import SupportMessage, SupportRoom
|
|
from apps.support.serializers import (
|
|
AdminSupportMessageSerializer,
|
|
AdminSupportRoomSerializer,
|
|
SendSupportMessageSerializer,
|
|
SupportMessageSerializer,
|
|
SupportRoomSerializer,
|
|
serialize_admin_chat_user,
|
|
)
|
|
|
|
User = get_user_model()
|
|
|
|
|
|
class IsSupportAdminPanelUser(BasePermission):
|
|
"""
|
|
Allows access to users who can log into the admin panel (super_admin, admin, professor, staff).
|
|
"""
|
|
|
|
def has_permission(self, request, view):
|
|
user = request.user
|
|
if not user or not user.is_authenticated:
|
|
return False
|
|
if getattr(user, 'is_superuser', False) or getattr(user, 'is_staff', False):
|
|
return True
|
|
if hasattr(user, 'can_access_admin_panel') and user.can_access_admin_panel():
|
|
return True
|
|
return getattr(user, 'user_type', None) in ('super_admin', 'admin', 'professor', 'consultant')
|
|
|
|
|
|
def cleanup_deleted_or_inactive_support_rooms() -> None:
|
|
"""
|
|
Deletes SupportRooms belonging to users who have been soft-deleted or deactivated.
|
|
"""
|
|
SupportRoom.objects.filter(
|
|
Q(user__is_active=False)
|
|
| Q(user__deleted_at__isnull=False)
|
|
| Q(user__fullname__endswith=':deleted')
|
|
).delete()
|
|
|
|
|
|
def active_support_rooms_queryset():
|
|
"""
|
|
Returns SupportRooms belonging only to active, non-deleted users (including active guest users)
|
|
that have at least one message.
|
|
"""
|
|
cleanup_deleted_or_inactive_support_rooms()
|
|
return (
|
|
SupportRoom.objects.filter(
|
|
user__is_active=True,
|
|
user__deleted_at__isnull=True,
|
|
messages__isnull=False,
|
|
)
|
|
.exclude(user__fullname__endswith=':deleted')
|
|
.distinct()
|
|
)
|
|
|
|
|
|
def get_total_admin_unread_count() -> int:
|
|
agg = (
|
|
SupportRoom.objects.filter(
|
|
user__is_active=True,
|
|
user__deleted_at__isnull=True,
|
|
messages__isnull=False,
|
|
)
|
|
.exclude(user__fullname__endswith=':deleted')
|
|
.distinct()
|
|
.aggregate(total=Sum('unread_by_admin'))
|
|
)
|
|
return int(agg.get('total') or 0)
|
|
|
|
|
|
def resolve_user_support_room(request, create_if_missing: bool = False):
|
|
"""
|
|
Resolves the SupportRoom for request.user (works for both logged-in and guest users).
|
|
If the user is logged in, automatically checks for and merges any unmerged guest support room
|
|
(from X-Guest-Token or Web IP+UA device_id).
|
|
"""
|
|
user = request.user
|
|
if not getattr(user, 'is_guest', False) and getattr(user, 'email', None):
|
|
from apps.account.guest_merge import resolve_guest_users_from_request, merge_guest_user_data
|
|
|
|
guest_candidates = resolve_guest_users_from_request(request)
|
|
for guest_user in guest_candidates:
|
|
if guest_user.id != user.id:
|
|
merge_guest_user_data(guest_user, user)
|
|
|
|
room = SupportRoom.objects.filter(user=user).first()
|
|
if room:
|
|
return room
|
|
|
|
if create_if_missing:
|
|
room, _ = SupportRoom.objects.get_or_create(user=user)
|
|
return room
|
|
|
|
return None
|
|
|
|
|
|
class SupportChatMessagesView(APIView):
|
|
"""
|
|
GET /api/support/messages/?after_id=<id>
|
|
Returns the user's support room and messages (all or after `after_id` for short polling).
|
|
POST /api/support/messages/
|
|
Sends a new message from the user (or guest user) to support.
|
|
"""
|
|
authentication_classes = [TokenAuthentication]
|
|
permission_classes = [IsAuthenticated]
|
|
|
|
def get(self, request, *args, **kwargs):
|
|
room = resolve_user_support_room(request, create_if_missing=False)
|
|
if not room:
|
|
return Response(
|
|
{
|
|
'room': None,
|
|
'messages': [],
|
|
},
|
|
status=status.HTTP_200_OK,
|
|
)
|
|
|
|
after_id_param = request.query_params.get('after_id')
|
|
messages_qs = room.messages.select_related('sender').order_by('id')
|
|
|
|
if after_id_param is not None:
|
|
try:
|
|
after_id = int(after_id_param)
|
|
if after_id > 0:
|
|
messages_qs = messages_qs.filter(id__gt=after_id)
|
|
except (TypeError, ValueError):
|
|
pass
|
|
|
|
messages_list = list(messages_qs)
|
|
|
|
# Mark support replies as read when user fetches messages in open chat
|
|
mark_read = request.query_params.get('mark_read', 'true').lower() != 'false'
|
|
if mark_read and (room.unread_by_user > 0 or any(
|
|
m.sender_type == SupportMessage.SenderType.SUPPORT and not m.is_read
|
|
for m in messages_list
|
|
)):
|
|
SupportMessage.objects.filter(
|
|
room=room,
|
|
sender_type=SupportMessage.SenderType.SUPPORT,
|
|
is_read=False,
|
|
).update(is_read=True)
|
|
if room.unread_by_user != 0:
|
|
room.unread_by_user = 0
|
|
room.save(update_fields=['unread_by_user', 'updated_at'])
|
|
|
|
return Response(
|
|
{
|
|
'room': SupportRoomSerializer(room).data,
|
|
'messages': SupportMessageSerializer(messages_list, many=True).data,
|
|
},
|
|
status=status.HTTP_200_OK,
|
|
)
|
|
|
|
@transaction.atomic
|
|
def post(self, request, *args, **kwargs):
|
|
serializer = SendSupportMessageSerializer(data=request.data)
|
|
serializer.is_valid(raise_exception=True)
|
|
|
|
room = resolve_user_support_room(request, create_if_missing=True)
|
|
message = SupportMessage.objects.create(
|
|
room=room,
|
|
sender_type=SupportMessage.SenderType.USER,
|
|
sender=request.user,
|
|
content=serializer.validated_data['content'],
|
|
)
|
|
room.refresh_from_db()
|
|
|
|
return Response(
|
|
{
|
|
'room': SupportRoomSerializer(room).data,
|
|
'message': SupportMessageSerializer(message).data,
|
|
},
|
|
status=status.HTTP_201_CREATED,
|
|
)
|
|
|
|
|
|
class SupportUnreadCountView(APIView):
|
|
"""
|
|
GET /api/support/unread/
|
|
Lightweight endpoint to check if the user has unread support specialist replies.
|
|
"""
|
|
authentication_classes = [TokenAuthentication]
|
|
permission_classes = [IsAuthenticated]
|
|
|
|
def get(self, request, *args, **kwargs):
|
|
room = resolve_user_support_room(request, create_if_missing=False)
|
|
return Response(
|
|
{
|
|
'unread_count': room.unread_by_user if room else 0,
|
|
'status': room.status if room else None,
|
|
},
|
|
status=status.HTTP_200_OK,
|
|
)
|
|
|
|
|
|
class AdminSupportRoomListView(APIView):
|
|
"""
|
|
GET /api/support/admin/rooms/?page=1&per_page=20&search=...
|
|
Lists all SupportRooms for the admin dashboard (`dovodi-panel`).
|
|
"""
|
|
authentication_classes = [TokenAuthentication]
|
|
permission_classes = [IsAuthenticated, IsSupportAdminPanelUser]
|
|
|
|
def get(self, request, *args, **kwargs):
|
|
try:
|
|
page = max(1, int(request.query_params.get('page', 1)))
|
|
except (TypeError, ValueError):
|
|
page = 1
|
|
|
|
try:
|
|
per_page = max(1, min(100, int(request.query_params.get('per_page', 20))))
|
|
except (TypeError, ValueError):
|
|
per_page = 20
|
|
|
|
search = (request.query_params.get('search') or '').strip()
|
|
|
|
qs = active_support_rooms_queryset().select_related('user', 'assigned_to').order_by(
|
|
'-last_message_at',
|
|
'-updated_at',
|
|
'-id',
|
|
)
|
|
|
|
if search:
|
|
q_filter = (
|
|
Q(user__fullname__icontains=search)
|
|
| Q(user__email__icontains=search)
|
|
| Q(user__phone_number__icontains=search)
|
|
| Q(messages__content__icontains=search)
|
|
)
|
|
digits_match = re.search(r'(\d+)', search)
|
|
if digits_match:
|
|
num_val = int(digits_match.group(1))
|
|
q_filter |= Q(id=num_val) | Q(user__id=num_val)
|
|
elif search.lower() == 'guest':
|
|
q_filter |= Q(user__email__isnull=True)
|
|
|
|
qs = qs.filter(q_filter).distinct()
|
|
|
|
total_rooms = qs.count()
|
|
total_pages = max(1, math.ceil(total_rooms / per_page))
|
|
offset = (page - 1) * per_page
|
|
rooms_page = list(qs[offset:offset + per_page])
|
|
|
|
serialized_rooms = AdminSupportRoomSerializer(
|
|
rooms_page,
|
|
many=True,
|
|
context={'request': request},
|
|
).data
|
|
|
|
return Response(
|
|
{
|
|
'status': 'success',
|
|
'current_user': serialize_admin_chat_user(request.user, request=request),
|
|
'room_messages': serialized_rooms,
|
|
'total_pages': total_pages,
|
|
'current_page': page,
|
|
'per_page': per_page,
|
|
'total_rooms': total_rooms,
|
|
'total_unread_count': get_total_admin_unread_count(),
|
|
},
|
|
status=status.HTTP_200_OK,
|
|
)
|
|
|
|
|
|
class AdminSupportRoomByUserView(APIView):
|
|
"""
|
|
POST /api/support/admin/rooms/by-user/
|
|
Body: { "user_id": <int> }
|
|
Gets or creates a SupportRoom for a specific user when an admin starts a new chat from the panel.
|
|
"""
|
|
authentication_classes = [TokenAuthentication]
|
|
permission_classes = [IsAuthenticated, IsSupportAdminPanelUser]
|
|
|
|
@transaction.atomic
|
|
def post(self, request, *args, **kwargs):
|
|
user_id = request.data.get('user_id') or request.data.get('recipient_id')
|
|
target_user = get_object_or_404(User, pk=user_id)
|
|
room, _ = SupportRoom.objects.get_or_create(
|
|
user=target_user,
|
|
defaults={'assigned_to': request.user},
|
|
)
|
|
|
|
# Mark any unread messages from this user as read
|
|
if room.unread_by_admin > 0:
|
|
SupportMessage.objects.filter(
|
|
room=room,
|
|
sender_type=SupportMessage.SenderType.USER,
|
|
is_read=False,
|
|
).update(is_read=True)
|
|
room.unread_by_admin = 0
|
|
room.save(update_fields=['unread_by_admin', 'updated_at'])
|
|
|
|
messages_qs = room.messages.select_related('sender').order_by('id')
|
|
messages_list = list(messages_qs)
|
|
|
|
return Response(
|
|
{
|
|
'status': 'success',
|
|
'room': AdminSupportRoomSerializer(room, context={'request': request}).data,
|
|
'messages': AdminSupportMessageSerializer(
|
|
messages_list,
|
|
many=True,
|
|
context={'request': request},
|
|
).data,
|
|
'current_user': serialize_admin_chat_user(request.user, request=request),
|
|
'total_pages': 1,
|
|
'current_page': 1,
|
|
'per_page': max(20, len(messages_list)),
|
|
'total_messages': len(messages_list),
|
|
'total_unread_count': get_total_admin_unread_count(),
|
|
},
|
|
status=status.HTTP_200_OK,
|
|
)
|
|
|
|
|
|
class AdminSupportRoomMessagesView(APIView):
|
|
"""
|
|
GET /api/support/admin/rooms/<int:room_id>/messages/?after_id=<id>
|
|
Returns messages for a SupportRoom and marks unread user messages as read.
|
|
POST /api/support/admin/rooms/<int:room_id>/messages/
|
|
Sends a reply from the admin/specialist (`sender_type='support'`).
|
|
"""
|
|
authentication_classes = [TokenAuthentication]
|
|
permission_classes = [IsAuthenticated, IsSupportAdminPanelUser]
|
|
|
|
def get(self, request, room_id: int, *args, **kwargs):
|
|
room = get_object_or_404(
|
|
active_support_rooms_queryset().select_related('user', 'assigned_to'),
|
|
pk=room_id,
|
|
)
|
|
|
|
after_id_param = request.query_params.get('after_id')
|
|
messages_qs = room.messages.select_related('sender').order_by('id')
|
|
|
|
if after_id_param is not None:
|
|
try:
|
|
after_id = int(after_id_param)
|
|
if after_id > 0:
|
|
messages_qs = messages_qs.filter(id__gt=after_id)
|
|
except (TypeError, ValueError):
|
|
pass
|
|
|
|
messages_list = list(messages_qs)
|
|
|
|
# Mark user messages as read when admin views the room
|
|
if room.unread_by_admin > 0 or any(
|
|
m.sender_type == SupportMessage.SenderType.USER and not m.is_read
|
|
for m in messages_list
|
|
):
|
|
SupportMessage.objects.filter(
|
|
room=room,
|
|
sender_type=SupportMessage.SenderType.USER,
|
|
is_read=False,
|
|
).update(is_read=True)
|
|
if room.unread_by_admin != 0:
|
|
room.unread_by_admin = 0
|
|
room.save(update_fields=['unread_by_admin', 'updated_at'])
|
|
|
|
total_messages = room.messages.count()
|
|
|
|
return Response(
|
|
{
|
|
'status': 'success',
|
|
'room': AdminSupportRoomSerializer(room, context={'request': request}).data,
|
|
'messages': AdminSupportMessageSerializer(
|
|
messages_list,
|
|
many=True,
|
|
context={'request': request},
|
|
).data,
|
|
'current_user': serialize_admin_chat_user(request.user, request=request),
|
|
'total_pages': 1,
|
|
'current_page': 1,
|
|
'per_page': max(50, total_messages),
|
|
'total_messages': total_messages,
|
|
'total_unread_count': get_total_admin_unread_count(),
|
|
},
|
|
status=status.HTTP_200_OK,
|
|
)
|
|
|
|
@transaction.atomic
|
|
def post(self, request, room_id: int, *args, **kwargs):
|
|
room = get_object_or_404(
|
|
active_support_rooms_queryset().select_related('user', 'assigned_to'),
|
|
pk=room_id,
|
|
)
|
|
serializer = SendSupportMessageSerializer(data=request.data)
|
|
serializer.is_valid(raise_exception=True)
|
|
|
|
if not room.assigned_to_id:
|
|
room.assigned_to = request.user
|
|
room.save(update_fields=['assigned_to', 'updated_at'])
|
|
|
|
message = SupportMessage.objects.create(
|
|
room=room,
|
|
sender_type=SupportMessage.SenderType.SUPPORT,
|
|
sender=request.user,
|
|
content=serializer.validated_data['content'],
|
|
)
|
|
room.refresh_from_db()
|
|
|
|
return Response(
|
|
{
|
|
'status': 'success',
|
|
'room': AdminSupportRoomSerializer(room, context={'request': request}).data,
|
|
'message': AdminSupportMessageSerializer(
|
|
message,
|
|
context={'request': request},
|
|
).data,
|
|
'current_user': serialize_admin_chat_user(request.user, request=request),
|
|
'total_unread_count': get_total_admin_unread_count(),
|
|
},
|
|
status=status.HTTP_201_CREATED,
|
|
)
|
|
|
|
|
|
class AdminSupportMessageDetailView(APIView):
|
|
"""
|
|
PATCH /api/support/admin/messages/<int:message_id>/
|
|
Updates the content of a support message.
|
|
DELETE /api/support/admin/messages/<int:message_id>/
|
|
Deletes a support message.
|
|
"""
|
|
authentication_classes = [TokenAuthentication]
|
|
permission_classes = [IsAuthenticated, IsSupportAdminPanelUser]
|
|
|
|
@transaction.atomic
|
|
def patch(self, request, message_id: int, *args, **kwargs):
|
|
message = get_object_or_404(
|
|
SupportMessage.objects.select_related('room', 'sender'),
|
|
pk=message_id,
|
|
)
|
|
serializer = SendSupportMessageSerializer(data=request.data)
|
|
serializer.is_valid(raise_exception=True)
|
|
|
|
message.content = serializer.validated_data['content']
|
|
message.save(update_fields=['content'])
|
|
|
|
return Response(
|
|
{
|
|
'status': 'success',
|
|
'room_id': message.room_id,
|
|
'message': AdminSupportMessageSerializer(
|
|
message,
|
|
context={'request': request},
|
|
).data,
|
|
'total_unread_count': get_total_admin_unread_count(),
|
|
},
|
|
status=status.HTTP_200_OK,
|
|
)
|
|
|
|
@transaction.atomic
|
|
def delete(self, request, message_id: int, *args, **kwargs):
|
|
message = get_object_or_404(
|
|
SupportMessage.objects.select_related('room'),
|
|
pk=message_id,
|
|
)
|
|
room = message.room
|
|
deleted_id = message.id
|
|
message.delete()
|
|
|
|
last_msg = room.messages.order_by('-id').first()
|
|
room.last_message_at = last_msg.created_at if last_msg else None
|
|
room.save(update_fields=['last_message_at', 'updated_at'])
|
|
|
|
return Response(
|
|
{
|
|
'status': 'success',
|
|
'room_id': room.id,
|
|
'deleted_message_id': deleted_id,
|
|
'room': AdminSupportRoomSerializer(room, context={'request': request}).data,
|
|
'total_unread_count': get_total_admin_unread_count(),
|
|
},
|
|
status=status.HTTP_200_OK,
|
|
)
|