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
1,012 changes: 1,005 additions & 7 deletions chats/admin.py

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions chats/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,6 @@ class ChatsConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "chats"
verbose_name = "Чаты"

def ready(self):
from chats import signals # noqa: F401
73 changes: 73 additions & 0 deletions chats/attachments.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
from django.db.models import Q
from django.http import Http404
from django.urls import reverse

from chats.models import DirectChat, FileToMessage, ProgramChannel, ProjectChat
from chats.permissions import accessible_chats
from files.downloads import download_stored_file


def get_accessible_chat(user, model, chat_id):
chat = accessible_chats(user, model).filter(pk=chat_id).first()
if chat is None:
raise Http404("Чат недоступен.")
return chat


def chat_attachments(chat):
message_field = chat_attachment_field(chat)
filters = {
field + "__isnull": True
for field in ATTACHMENT_FIELDS.values()
if field != message_field
}
filters[message_field + "__chat"] = chat
filters[message_field + "__is_deleted"] = False
return FileToMessage.objects.filter(**filters)


ATTACHMENT_FIELDS = {
DirectChat: "direct_message",
ProjectChat: "project_message",
ProgramChannel: "channel_message",
}
ATTACHMENT_IN_USE = "Файл прикреплен к опубликованному сообщению."


def used_by_visible_chat_message(file_id, *, using=None):
return (
FileToMessage.objects.using(using)
.filter(file_id=file_id)
.filter(
Q(direct_message__is_deleted=False)
| Q(project_message__is_deleted=False)
| Q(channel_message__is_deleted=False)
)
.exists()
)


def chat_attachment_field(chat):
return ATTACHMENT_FIELDS[type(chat)]


def attachment_download_url(attachment, message):
if attachment.channel_message_id is not None:
kind = "program-channel"
elif attachment.project_message_id is not None:
kind = "project-chat"
else:
kind = "direct-chat"
return reverse(
"chats:" + kind + "-attachment-download",
args=[message.chat_id, message.pk, attachment.download_id],
)


def download_chat_attachment(request, chat, message_id, attachment_id):
message_field = chat_attachment_field(chat) + "_id"
attachments = chat_attachments(chat).filter(**{message_field: message_id})
for attachment in attachments.select_related("file"):
if attachment.download_id == attachment_id:
return download_stored_file(request, attachment.file)
raise Http404("Вложение недоступно.")
26 changes: 26 additions & 0 deletions chats/channel_events.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from asgiref.sync import async_to_sync
from channels.layers import get_channel_layer
from django.db import transaction

from chats.websockets_settings import EventGroupType, EventType


def channel_group(channel_id):
return f"{EventGroupType.PROGRAM_CHANNEL}_{channel_id}"


def notify_channel_change(channel_id, *, new_message_id=None):
layer = get_channel_layer()
if layer is None:
return
transaction.on_commit(
lambda: async_to_sync(layer.group_send)(
channel_group(channel_id),
{
"type": EventType.PROGRAM_CHANNEL_CHANGED,
"content": {"channel_id": channel_id},
"_new_message_id": new_message_id,
},
),
robust=True,
)
228 changes: 228 additions & 0 deletions chats/channel_views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
from django.db.models import Exists, OuterRef, Prefetch
from django.shortcuts import get_object_or_404
from django.utils.decorators import method_decorator
from django.views.decorators.cache import never_cache
from drf_yasg.utils import swagger_auto_schema
from rest_framework import serializers, status
from rest_framework.exceptions import PermissionDenied, ValidationError
from rest_framework.generics import GenericAPIView, ListAPIView, RetrieveAPIView
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response

from chats.attachments import get_accessible_chat
from chats.drafts import with_draft_marker
from chats.exceptions import MessageValidationException
from chats.models import ProgramChannel, ProgramChannelMessage
from chats.notifications import MAX_MENTIONS
from chats.pagination import MessageListPagination
from chats.permissions import accessible_chats, can_publish_in_program_channel
from chats.program_channels import (
MAX_CHANNEL_ATTACHMENTS,
mark_channel_read,
publish_channel_message,
with_channel_unreads,
)
from chats.serializers import ProgramChannelMessageSerializer
from chats.services.messages import delete_message, edit_message
from chats.views import ChatMessageHistory
from partner_programs.models import PartnerProgram


class ProgramChannelSerializer(serializers.ModelSerializer):
id = serializers.IntegerField(source="pk")
name = serializers.CharField(source="program.name")
image_address = serializers.CharField(source="program.image_address", allow_null=True)
unread_count = serializers.IntegerField()
first_unread_message_id = serializers.IntegerField(allow_null=True)
can_publish = serializers.BooleanField(read_only=True)
has_draft = serializers.SerializerMethodField()
last_message = serializers.SerializerMethodField()

def get_has_draft(self, channel) -> bool:
return channel.can_publish and channel.has_draft

def get_last_message(self, channel):
if channel.latest_messages:
return ProgramChannelMessageSerializer(
channel.latest_messages[0], context=self.context
).data
return None

class Meta:
model = ProgramChannel
fields = (
"id",
"name",
"image_address",
"can_publish",
"unread_count",
"first_unread_message_id",
"last_message",
"has_draft",
)


class ChannelMessageInput(serializers.Serializer):
text = serializers.CharField(
required=False, default="", allow_blank=True, trim_whitespace=False
)
file_urls = serializers.ListField(
child=serializers.CharField(max_length=200),
required=False,
default=list,
max_length=MAX_CHANNEL_ATTACHMENTS,
)
client_message_id = serializers.UUIDField(
required=False, allow_null=True, default=None
)
mentioned_user_ids = serializers.ListField(
child=serializers.IntegerField(min_value=1, max_value=2**63 - 1),
required=False,
default=list,
max_length=MAX_MENTIONS,
)


class ChannelEditInput(serializers.Serializer):
text = serializers.CharField(allow_blank=True, trim_whitespace=False)


class ChannelReadInput(serializers.Serializer):
message_id = serializers.IntegerField(min_value=1, max_value=2**63 - 1)


class ProgramChannelSummary:
permission_classes = [IsAuthenticated]
serializer_class = ProgramChannelSerializer
pagination_class = MessageListPagination

def get_queryset(self):
if getattr(self, "swagger_fake_view", False):
return ProgramChannel.objects.none()
user = self.request.user
managed = PartnerProgram.objects.filter(pk=OuterRef("pk"), managers=user)
channels = with_draft_marker(
accessible_chats(user, ProgramChannel).select_related("program"), user
)
recent = (
ProgramChannelMessage.objects.filter(is_deleted=False)
.select_related("author")
.prefetch_related("file_to_message__file")
.order_by("-created_at", "-pk")[:1]
)
return (
with_channel_unreads(channels, user.pk)
.annotate(can_publish=Exists(managed))
.prefetch_related(
Prefetch("messages", queryset=recent, to_attr="latest_messages")
)
.order_by("program__name", "pk")
)


@method_decorator(never_cache, name="dispatch")
class ProgramChannelList(ProgramChannelSummary, ListAPIView):
pass


@method_decorator(never_cache, name="dispatch")
class ProgramChannelDetail(ProgramChannelSummary, RetrieveAPIView):
lookup_url_kwarg = "id"


@method_decorator(never_cache, name="dispatch")
class ProgramChannelHistory(ChatMessageHistory):
chat_model = ProgramChannel
permission_classes = [IsAuthenticated]
serializer_class = ProgramChannelMessageSerializer

def get_queryset(self):
return (
super()
.get_queryset()
.select_related("author")
.prefetch_related("file_to_message__file")
)

@swagger_auto_schema(
request_body=ChannelMessageInput,
responses={
200: ProgramChannelMessageSerializer,
201: ProgramChannelMessageSerializer,
},
)
def post(self, request, id):
require_publisher(request.user, id)
form = ChannelMessageInput(data=request.data)
form.is_valid(raise_exception=True)
try:
publication, created = publish_channel_message(
channel_id=id, author=request.user, **form.validated_data
)
except MessageValidationException as error:
raise ValidationError({"detail": str(error)}) from error
return Response(
publication, status=status.HTTP_201_CREATED if created else status.HTTP_200_OK
)


def require_publisher(user, channel_id):
channel = get_accessible_chat(user, ProgramChannel, channel_id)
if not can_publish_in_program_channel(user, channel.program):
raise PermissionDenied("Публикации доступны менеджерам программы.")
return channel


@method_decorator(never_cache, name="dispatch")
class ProgramChannelMessageChange(GenericAPIView):
permission_classes = [IsAuthenticated]
serializer_class = ChannelEditInput

def get_change(self, request, channel_id, message_id):
channel = require_publisher(request.user, channel_id)
get_object_or_404(channel.visible_messages(), pk=message_id, author=request.user)
return dict(
chat_model=ProgramChannel,
chat_id=channel_id,
message_id=message_id,
author=request.user,
)

@swagger_auto_schema(
request_body=ChannelEditInput, responses={200: ProgramChannelMessageSerializer}
)
def patch(self, request, id, message_id):
change = self.get_change(request, id, message_id)
form = self.get_serializer(data=request.data)
form.is_valid(raise_exception=True)
try:
publication = edit_message(**change, **form.validated_data)
except MessageValidationException as error:
raise ValidationError({"detail": str(error)}) from error
return Response(publication)

def delete(self, request, id, message_id):
change = self.get_change(request, id, message_id)
try:
delete_message(**change)
except MessageValidationException as error:
raise ValidationError({"detail": str(error)}) from error
return Response(status=status.HTTP_204_NO_CONTENT)


@method_decorator(never_cache, name="dispatch")
class ProgramChannelReadView(GenericAPIView):
permission_classes = [IsAuthenticated]
serializer_class = ChannelReadInput

def post(self, request, id):
get_accessible_chat(request.user, ProgramChannel, id)
form = self.get_serializer(data=request.data)
form.is_valid(raise_exception=True)
try:
last_message_id = mark_channel_read(
channel_id=id, user=request.user, **form.validated_data
)
except MessageValidationException as error:
raise ValidationError({"detail": str(error)}) from error
return Response({"last_message_id": last_message_id})
Loading
Loading