Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 40 additions & 1 deletion chats/admin.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
from django.contrib import admin
from functools import partial

from asgiref.sync import async_to_sync
from django.contrib import admin, messages
from django.db import transaction

from chats.exceptions import MessageValidationException
from chats.models import (
ProjectChat,
DirectChat,
ProjectChatMessage,
DirectChatMessage,
FileToMessage,
)
from chats.project_reads import mark_project_message_read, publish_project_message_read


@admin.display(description="Пользователи чата")
Expand Down Expand Up @@ -38,6 +44,39 @@ class DirectChatAdmin(admin.ModelAdmin):
class ProjectChatMessageAdmin(admin.ModelAdmin):
list_display = ("id", "author", "chat", "created_at")
list_display_links = ("id", "author", "chat")
exclude = ("is_read",)
actions = ("mark_read_for_me",)

@admin.action(permissions=["view"], description="Отметить как прочитанные для себя")
def mark_read_for_me(self, request, queryset):
try:
with transaction.atomic():
marked = 0
for chat_id, message_id in queryset.order_by("pk").values_list(
"chat_id", "pk"
):
author_id, read_count, created = mark_project_message_read(
chat_id=chat_id, message_id=message_id, user=request.user
)
if created:
marked += 1
transaction.on_commit(
partial(
async_to_sync(publish_project_message_read),
chat_id=chat_id,
message_id=message_id,
user_id=request.user.pk,
author_id=author_id,
read_count=read_count,
),
robust=True,
)
except MessageValidationException as error:
self.message_user(request, str(error), level=messages.ERROR)
else:
self.message_user(
request, f"Отмечено прочитанными: {marked}", level=messages.SUCCESS
)


@admin.register(DirectChatMessage)
Expand Down
26 changes: 17 additions & 9 deletions chats/consumers/chat.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

from chats.exceptions import ChatException
from chats.models import BaseChat
from chats.project_reads import personalize_project_message_event
from chats.utils import get_user_channel_cache_key
from chats.websockets_settings import (
Event,
Expand Down Expand Up @@ -157,20 +158,18 @@ async def receive_json(self, content, **kwargs):
self.user, self.channel_layer, self.channel_name
)

room_name = f"{EventGroupType.CHATS_RELATED}_{event.content.get('chat_id')}"
chat_id = event.content.get("chat_id")
if event.content["chat_type"] == ChatType.PROJECT:
chat_id = int(chat_id)
room_name = f"{EventGroupType.CHATS_RELATED}_{chat_id}"
if event.content["chat_type"] == ChatType.PROJECT:
await self._ensure_room_subscription(room_name)
try:
await self.__process_chat_related_event(event, room_name)
except ChatException as e:
await self.send_json({"error": str(e.get_error())})
except KeyError as e:
await self.send_json(
{
"error": f"Missing key (might be backend's fault,"
f" but most likely you are missing this field): {e}"
}
)
await self.send_json({"error": f"В запросе отсутствует поле {e}."})

elif event.type in [EventType.SET_ONLINE, EventType.SET_OFFLINE]:
room_name = EventGroupType.GENERAL_EVENTS
Expand Down Expand Up @@ -228,15 +227,24 @@ async def _send_chat_event(self, event):
chat_id = event.get("_project_chat_id")
if chat_id is not None:
if not self.user or not await can_access_project_chat(self.user.pk, chat_id):
room_name = f"{EventGroupType.CHATS_RELATED}_{chat_id}"
room_name = f"{EventGroupType.CHATS_RELATED}_{int(chat_id)}"
await self.channel_layer.group_discard(room_name, self.channel_name)
self.joined_rooms.discard(room_name)
return
audience = event.get("_project_read_audience")
if audience is not None and self.user.pk not in audience:
return
if event["type"] in (EventType.NEW_MESSAGE, EventType.EDIT_MESSAGE):
content = event["content"]
message = await database_sync_to_async(personalize_project_message_event)(
content["message"], self.user.pk
)
event = {**event, "content": {**content, "message": message}}
# Служебный контекст нужен для проверки уже открытых сокетов,
# клиентский формат сообщений остается прежним.
await self.send(
json.dumps(
{key: value for key, value in event.items() if key != "_project_chat_id"}
{key: value for key, value in event.items() if not key.startswith("_")}
)
)

Expand Down
64 changes: 25 additions & 39 deletions chats/consumers/event_types/ProjectEvent.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from channels.db import database_sync_to_async

from chats.models import ProjectChat, ProjectChatMessage
from chats.project_reads import mark_project_message_read, publish_project_message_read
from chats.utils import (
orm_exists,
orm_get,
Expand All @@ -9,7 +10,6 @@
from chats.services import create_project_message
from chats.websockets_settings import Event, EventType
from chats.exceptions import (
WrongChatIdException,
UserNotInChatException,
UserNotMessageAuthorException,
UserIsNotAuthor,
Expand Down Expand Up @@ -55,37 +55,20 @@ async def process_new_message_event(self, event: Event, room_name: str):
await self.channel_layer.send(self.channel_name, event_data)

async def process_read_message_event(self, event: Event, room_name: str):
msg = await orm_get(ProjectChatMessage.objects, pk=event.content["message_id"])
chat = await orm_get(
ProjectChat.objects.select_related("project__leader"),
pk=msg.chat_id,
)
# check that user is in this chat
is_member = chat.project.leader_id == self.user.id or await orm_exists(
Collaborator.objects.filter(project_id=chat.project_id, user_id=self.user.id)
)
if not is_member:
raise UserNotInChatException(
f"User {self.user.id} is not in project chat {msg.chat_id}"
)
if msg.chat_id != int(event.content["chat_id"]):
raise WrongChatIdException(
"Some of chat/message ids are wrong, you can't access this message"
)
msg.is_read = True
await orm_save(msg, update_fields=["is_read"])
await self.channel_layer.group_send(
room_name,
{
"type": EventType.READ_MESSAGE,
"content": {
"chat_id": event.content["chat_id"],
"chat_type": event.content["chat_type"],
"user_id": self.user.id,
"message_id": event.content["message_id"],
},
"_project_chat_id": event.content["chat_id"],
},
chat_id = event.content["chat_id"]
message_id = event.content["message_id"]
author_id, read_count, created = await database_sync_to_async(
mark_project_message_read
)(chat_id=chat_id, message_id=message_id, user=self.user)
if author_id == self.user.pk:
return
await publish_project_message_read(
chat_id=chat_id,
message_id=int(message_id),
user_id=self.user.id,
author_id=author_id,
read_count=read_count,
sender_channel=None if created else self.channel_name,
)

async def process_delete_message_event(self, event: Event, room_name: str):
Expand Down Expand Up @@ -150,13 +133,7 @@ async def process_edit_message_event(self, event, room_name):
message.is_edited = True
await orm_save(message, update_fields=["text", "is_edited"])

serialized_message = await orm_get(
ProjectChatMessage.objects.select_related(
"author", "reply_to__author"
).prefetch_related("file_to_message__file"),
pk=message.pk,
)
message_data = ProjectChatMessageListSerializer(serialized_message).data
message_data = await database_sync_to_async(serialize_project_message)(message.pk)
content = {
"chat_id": chat_id,
"message": message_data,
Expand All @@ -169,3 +146,12 @@ async def process_edit_message_event(self, event, room_name):
}

await self.channel_layer.group_send(room_name, event_data)


def serialize_project_message(message_id):
message = (
ProjectChatMessage.objects.select_related("author", "reply_to__author")
.prefetch_related("file_to_message__file")
.get(pk=message_id)
)
return ProjectChatMessageListSerializer(message).data
55 changes: 55 additions & 0 deletions chats/migrations/0013_project_chat_message_reads.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Generated by Django 5.2.18 on 2026-11-13 16:56

import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("chats", "0012_directchatmessage_client_message_id_and_more"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]

operations = [
migrations.CreateModel(
name="ProjectChatMessageRead",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"message",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="read_receipts",
to="chats.projectchatmessage",
),
),
(
"user",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL,
),
),
],
options={
"verbose_name": "Прочтение сообщения в чате проекта",
"verbose_name_plural": "Прочтения сообщений в чатах проектов",
"constraints": [
models.UniqueConstraint(
fields=("message", "user"),
name="unique_project_chat_message_reader",
)
],
},
),
]
16 changes: 16 additions & 0 deletions chats/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,22 @@ class Meta:
]


class ProjectChatMessageRead(models.Model):
message = models.ForeignKey(
ProjectChatMessage, on_delete=models.CASCADE, related_name="read_receipts"
)
user = models.ForeignKey(User, on_delete=models.CASCADE)

class Meta:
verbose_name = "Прочтение сообщения в чате проекта"
verbose_name_plural = "Прочтения сообщений в чатах проектов"
constraints = [
models.UniqueConstraint(
fields=["message", "user"], name="unique_project_chat_message_reader"
)
]


class DirectChatMessage(BaseMessage):
"""
DirectChatMessage model
Expand Down
Loading
Loading