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
36 changes: 28 additions & 8 deletions chats/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@
from django.core.exceptions import PermissionDenied
from django.core.paginator import Paginator
from django.db import transaction
from django.shortcuts import get_object_or_404
from django.template.response import TemplateResponse
from django.urls import path, reverse
from django.utils.html import format_html
from django.views.decorators.http import require_safe

from chats.attachments import download_chat_attachment, get_accessible_chat
from chats.exceptions import MessageValidationException
from chats.models import (
ProjectChat,
Expand All @@ -19,7 +20,7 @@
FileToMessage,
)
from chats.project_reads import mark_project_message_read, publish_project_message_read
from chats.search import ChatMessageSearchForm, accessible_chats, search_chat_messages
from chats.search import ChatMessageSearchForm, search_chat_messages


@admin.display(description="Пользователи чата")
Expand All @@ -40,13 +41,21 @@ def message_search_url_name(self):
)

def get_urls(self):
converter = "int" if self.model is ProjectChat else "slug"
object_path = (
"<int:object_id>" if self.model is ProjectChat else "<slug:object_id>"
)
return [
path(
"<" + converter + ":object_id>/message-search/",
object_path + "/message-search/",
self.admin_site.admin_view(self.message_search),
name=self.message_search_url_name,
),
path(
object_path
+ "/messages/<int:message_id>/files/<slug:attachment_id>/download/",
self.admin_site.admin_view(require_safe(self.attachment_download)),
name=self.message_search_url_name + "_download",
),
] + super().get_urls()

@admin.display(description="Поиск по переписке")
Expand All @@ -59,10 +68,7 @@ def message_search_link(self, chat):
return format_html('<a href="{}">Найти в чате</a>', url)

def message_search(self, request, object_id):
visible = accessible_chats(request.user, self.model)
chat = get_object_or_404(visible, pk=object_id)
if not self.has_view_or_change_permission(request, chat):
raise PermissionDenied
chat = self.get_chat_for_member(request, object_id)
form = ChatMessageSearchForm(request.GET)
page = None
if form.is_valid():
Expand All @@ -82,9 +88,23 @@ def message_search(self, request, object_id):
"title": "Поиск по переписке",
"form": form,
"page": page,
"chat": chat,
"attachment_download_route": "admin:"
+ self.message_search_url_name
+ "_download",
},
)

def get_chat_for_member(self, request, object_id):
chat = get_accessible_chat(request.user, self.model, object_id)
if not self.has_view_or_change_permission(request, chat):
raise PermissionDenied
return chat

def attachment_download(self, request, object_id, message_id, attachment_id):
chat = self.get_chat_for_member(request, object_id)
return download_chat_attachment(request, chat, message_id, attachment_id)


@admin.register(ProjectChat)
class ChatAdmin(ChatSearchAdmin):
Expand Down
46 changes: 46 additions & 0 deletions chats/attachments.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
from django.http import Http404
from django.urls import reverse

from chats.models import FileToMessage, 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):
if isinstance(chat, ProjectChat):
return FileToMessage.objects.filter(
project_message__chat=chat,
project_message__is_deleted=False,
direct_message__isnull=True,
)
return FileToMessage.objects.filter(
direct_message__chat=chat,
direct_message__is_deleted=False,
project_message__isnull=True,
)


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


def download_chat_attachment(request, chat, message_id, attachment_id):
message_field = (
"project_message_id" if isinstance(chat, ProjectChat) else "direct_message_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("Вложение недоступно.")
9 changes: 9 additions & 0 deletions chats/models.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from abc import abstractmethod
from hashlib import sha256
from typing import List

from django.contrib.auth import get_user_model
Expand Down Expand Up @@ -331,6 +332,14 @@ class FileToMessage(models.Model):
null=True,
)

@property
def download_id(self):
return sha256(self.file_id.encode()).hexdigest()

@property
def is_linked_to_one_message(self):
return (self.direct_message_id is None) != (self.project_message_id is None)

def __str__(self):
return f"FileToMessage<{self.file}>"

Expand Down
10 changes: 10 additions & 0 deletions chats/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,16 @@
User = get_user_model()


def accessible_chats(user, chat_model):
from chats.models import DirectChat

if not user.is_authenticated:
return chat_model.objects.none()
if chat_model is DirectChat:
return user.direct_chats.all()
return user.get_project_chats()


def get_lookup_kwarg(request, view, *names):
view_kwargs = getattr(view, "kwargs", {}) or {}
parser_kwargs = (getattr(request, "parser_context", None) or {}).get("kwargs", {})
Expand Down
10 changes: 1 addition & 9 deletions chats/search.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from django.db.models import Case, Exists, F, OuterRef, Q, TextField, Value, When
from django.db.models.functions import Concat

from chats.models import DirectChat, ProjectChat
from chats.models import ProjectChat
from core.search import unicode_literal_pattern
from files.models import UserFile

Expand Down Expand Up @@ -44,14 +44,6 @@ def __init__(self, data):
self.order_fields(["q", "scope", "author_id"])


def accessible_chats(user, chat_model):
if not user.is_authenticated:
return chat_model.objects.none()
if chat_model is DirectChat:
return user.direct_chats.all()
return user.get_project_chats()


def search_chat_messages(chat, filters):
query = filters["q"]
scope = filters["scope"]
Expand Down
51 changes: 41 additions & 10 deletions chats/serializers.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
from rest_framework import serializers

from chats.attachments import attachment_download_url
from chats.models import (
DirectChat,
FileToMessage,
ProjectChat,
DirectChatMessage,
ProjectChatMessage,
Expand All @@ -11,6 +13,43 @@
from users.serializers import UserListSerializer, UserDetailSerializer, UserChatSerializer


class ChatAttachmentSerializer(serializers.ModelSerializer):
name = serializers.CharField(source="file.name", read_only=True)
extension = serializers.CharField(source="file.extension", read_only=True)
mime_type = serializers.CharField(source="file.mime_type", read_only=True)
size = serializers.IntegerField(source="file.size", read_only=True)
user = serializers.IntegerField(
source="file.user_id", read_only=True, allow_null=True
)
datetime_uploaded = serializers.DateTimeField(
source="file.datetime_uploaded", read_only=True
)
link = serializers.SerializerMethodField()

def get_link(self, attachment) -> str:
message = self.context.get("message") or (
attachment.project_message
if attachment.project_message_id is not None
else attachment.direct_message
)
return attachment_download_url(attachment, message)

class Meta:
model = FileToMessage
fields = UserFileSerializer.Meta.fields


def serialize_message_attachments(message):
if message.is_deleted:
return []
attachments = [
item for item in message.file_to_message.all() if item.is_linked_to_one_message
]
return ChatAttachmentSerializer(
attachments, many=True, context={"message": message}
).data


class DirectChatListSerializer(serializers.ModelSerializer):
last_message = serializers.SerializerMethodField()
opponent = serializers.SerializerMethodField()
Expand Down Expand Up @@ -162,11 +201,7 @@ class DirectChatMessageListSerializer(serializers.ModelSerializer):

@classmethod
def get_files(cls, message: DirectChatMessage):
data = []
for file_to_message in message.file_to_message.all():
file_data = UserFileSerializer(file_to_message.file).data
data.append(file_data)
return data
return serialize_message_attachments(message)

class Meta:
model = DirectChatMessage
Expand Down Expand Up @@ -227,11 +262,7 @@ class ProjectChatMessageListSerializer(ProjectChatMessageSerializer):

@classmethod
def get_files(cls, message: ProjectChatMessage):
data = []
for file_to_message in message.file_to_message.all():
file_data = UserFileSerializer(file_to_message.file).data
data.append(file_data)
return data
return serialize_message_attachments(message)

class Meta:
model = ProjectChatMessage
Expand Down
Loading
Loading