Merge branch 'develop'

This commit is contained in:
lizerk 2026-04-03 22:12:48 +03:00
commit 26d69fb718
3 changed files with 94 additions and 11 deletions

View File

@ -106,6 +106,38 @@ def _build_message_search_snippet(message, query, max_length=140):
return 'Сообщение без текста'
def _filter_messages_before_anchor(queryset, chat, before_id):
try:
anchor_id = int(before_id)
except (TypeError, ValueError):
return queryset
anchor = chat.messages.only('id', 'created_at').filter(pk=anchor_id).first()
if not anchor:
return queryset.filter(id__lt=anchor_id)
return queryset.filter(
Q(created_at__lt=anchor.created_at)
| Q(created_at=anchor.created_at, id__lt=anchor.id)
)
def _filter_messages_after_anchor(queryset, chat, after_id):
try:
anchor_id = int(after_id)
except (TypeError, ValueError):
return queryset
anchor = chat.messages.only('id', 'created_at').filter(pk=anchor_id).first()
if not anchor:
return queryset.filter(id__gt=anchor_id)
return queryset.filter(
Q(created_at__gt=anchor.created_at)
| Q(created_at=anchor.created_at, id__gt=anchor.id)
)
class ChatViewSet(viewsets.ModelViewSet):
"""
ViewSet для управления чатами
@ -675,28 +707,22 @@ class ChatViewSet(viewsets.ModelViewSet):
if after_id or after_ts:
# Для загрузки новых сообщений - сортируем по возрастанию (от старых к
# новым)
queryset = queryset.order_by('created_at')
queryset = queryset.order_by('created_at', 'id')
else:
# Для загрузки старых или начальной загрузки - по убыванию (от новых к
# старым)
queryset = queryset.order_by('-created_at')
queryset = queryset.order_by('-created_at', '-id')
# Фильтрация по ID (приоритет) или timestamp
if before_id:
try:
queryset = queryset.filter(id__lt=int(before_id))
except ValueError:
pass
queryset = _filter_messages_before_anchor(queryset, chat, before_id)
elif before_ts:
before_dt = _coerce_ts(before_ts)
if before_dt:
queryset = queryset.filter(created_at__lt=before_dt)
if after_id:
try:
queryset = queryset.filter(id__gt=int(after_id))
except ValueError:
pass
queryset = _filter_messages_after_anchor(queryset, chat, after_id)
elif after_ts:
after_dt = _coerce_ts(after_ts)
if after_dt:

View File

@ -6,6 +6,7 @@ import pytest
from communications.models import Chat, ChatMembership, ChatReadState, ChatUserSettings, Message
from django.contrib.auth import get_user_model
from django.urls import reverse
from django.utils import timezone
from employees.models import Department
from rest_framework import status
from rest_framework.test import APIClient
@ -328,6 +329,62 @@ class TestChatViewSet:
assert response.status_code == status.HTTP_200_OK
assert len(response.data["messages"]) <= 10
def test_messages_pagination_before_id_respects_created_at_anchor(
self, auth_client, private_chat, user1
):
"""before_id должен пагинировать по временной ленте, а не по голому id."""
msg_old = Message.objects.create(
chat=private_chat, author=user1, content="Old by time"
)
msg_middle = Message.objects.create(
chat=private_chat, author=user1, content="Middle by time"
)
msg_new = Message.objects.create(
chat=private_chat, author=user1, content="New by time"
)
base = timezone.now()
Message.objects.filter(pk=msg_old.pk).update(created_at=base)
Message.objects.filter(pk=msg_middle.pk).update(created_at=base + timezone.timedelta(minutes=20))
Message.objects.filter(pk=msg_new.pk).update(created_at=base + timezone.timedelta(minutes=10))
url = f"/api/v1/communications/chats/{private_chat.pk}/messages/"
response = auth_client.get(url, {"before_id": msg_middle.id, "limit": 10})
assert response.status_code == status.HTTP_200_OK
returned_ids = [item["id"] for item in response.data["messages"]]
assert msg_old.id in returned_ids
assert msg_new.id in returned_ids
assert msg_middle.id not in returned_ids
def test_messages_pagination_after_id_respects_created_at_anchor(
self, auth_client, private_chat, user1
):
"""after_id должен догружать по временной ленте даже при несогласованных id и created_at."""
msg_old = Message.objects.create(
chat=private_chat, author=user1, content="Old by time"
)
msg_middle = Message.objects.create(
chat=private_chat, author=user1, content="Middle by time"
)
msg_new = Message.objects.create(
chat=private_chat, author=user1, content="New by time"
)
base = timezone.now()
Message.objects.filter(pk=msg_old.pk).update(created_at=base)
Message.objects.filter(pk=msg_middle.pk).update(created_at=base + timezone.timedelta(minutes=20))
Message.objects.filter(pk=msg_new.pk).update(created_at=base + timezone.timedelta(minutes=10))
url = f"/api/v1/communications/chats/{private_chat.pk}/messages/"
response = auth_client.get(url, {"after_id": msg_new.id, "limit": 10})
assert response.status_code == status.HTTP_200_OK
returned_ids = [item["id"] for item in response.data["messages"]]
assert returned_ids == [msg_middle.id]
def test_chat_access_denied(self, user3, private_chat):
"""Попытка доступа к чату без прав"""
client = APIClient()

View File

@ -419,7 +419,7 @@ function MessageDialogPageContent() {
const viewport = c.containerRef.current;
const onScroll = () => {
if (!cm.loadingOlder && !cm.messagesLoading && cm.hasMoreOlder && viewport.scrollTop <= 120) {
if (!cm.loadingOlder && !cm.messagesLoading && (cm.hasMoreOlder || cm.allowOneOlderProbe) && viewport.scrollTop <= 120) {
cm.loadOlderMessages();
}