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.
 
 
 
 

458 lines
16 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 get_total_admin_unread_count() -> int:
agg = SupportRoom.objects.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 a logged-in user does not yet have a room, checks the optional X-Guest-Token header
to seamlessly migrate their existing guest support conversation.
"""
user = request.user
room = SupportRoom.objects.filter(user=user).first()
if room:
return room
# Check if user recently logged in from a guest session that already had a support chat
guest_token_key = request.headers.get('X-Guest-Token', '').strip()
if guest_token_key and not getattr(user, 'is_guest', False):
guest_token_obj = (
Token.objects.select_related('user')
.filter(key=guest_token_key)
.first()
)
if guest_token_obj and guest_token_obj.user_id != user.id:
guest_room = SupportRoom.objects.filter(user=guest_token_obj.user).first()
if guest_room:
guest_room.user = user
guest_room.save(update_fields=['user', 'updated_at'])
return guest_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 = SupportRoom.objects.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(
SupportRoom.objects.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(
SupportRoom.objects.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,
)