diff --git a/chats/apps/discussions/models/discussion_message.py b/chats/apps/discussions/models/discussion_message.py index 18f34c20..7a97dafa 100644 --- a/chats/apps/discussions/models/discussion_message.py +++ b/chats/apps/discussions/models/discussion_message.py @@ -34,8 +34,11 @@ def media(self): @property def serialized_ws_data(self): - # TODO: add serializer when creating message endpoints - return {} + from ..serializers.discussion_messages import ( # noqa + DiscussionReadMessageSerializer, + ) + + return DiscussionReadMessageSerializer(self).data @property def notification_groups(self) -> list: diff --git a/chats/apps/discussions/serializers/__init__.py b/chats/apps/discussions/serializers/__init__.py index d56ba485..43d408dd 100644 --- a/chats/apps/discussions/serializers/__init__.py +++ b/chats/apps/discussions/serializers/__init__.py @@ -1,4 +1,7 @@ from .discussion_users import DiscussionUserListSerializer # noqa +from .discussion_messages import DiscussionCreateMessageSerializer # noqa +from .discussion_messages import DiscussionReadMessageSerializer # noqa +from .discussion_messages import MessageAndMediaSimpleSerializer # noqa from .discussions import DiscussionCreateSerializer # noqa from .discussions import DiscussionDetailSerializer # noqa from .discussions import DiscussionListSerializer # noqa diff --git a/chats/apps/discussions/serializers/discussion_messages.py b/chats/apps/discussions/serializers/discussion_messages.py new file mode 100644 index 00000000..0bbaaa5f --- /dev/null +++ b/chats/apps/discussions/serializers/discussion_messages.py @@ -0,0 +1,56 @@ +from rest_framework import serializers + +from chats.apps.api.v1.accounts.serializers import UserNameEmailSerializer + +from ..models import DiscussionMessage, DiscussionMessageMedia + + +class DiscussionCreateMessageSerializer(serializers.Serializer): + text = serializers.CharField(required=True) + + +""" + { + "content_type": "audio/wav", + "created_on": "2022-12-15T18:06:45.654327-03:00", + "message": "28e04b5a-9e70-4826-bd24-fed837661495", + "url": "http://domain.com/recording.wav" + } +""" + + +class MessageMediaSimpleSerializer(serializers.ModelSerializer): + url = serializers.SerializerMethodField(read_only=True) + + class Meta: + model = DiscussionMessageMedia + fields = [ + "content_type", + "url", + "created_on", + ] + + def get_url(self, media: DiscussionMessageMedia): + return media.url + + +class DiscussionReadMessageSerializer(serializers.ModelSerializer): + user = UserNameEmailSerializer(many=False, required=False, read_only=True) + media = MessageMediaSimpleSerializer(many=True, required=False) + + class Meta: + model = DiscussionMessage + fields = [ + "uuid", + "user", + "discussion", + "text", + "media", + "created_on", + ] + + +class MessageAndMediaSimpleSerializer(serializers.Serializer): + text = serializers.CharField(required=False) + content_type = serializers.CharField(required=True) + media_file = serializers.FileField(required=True) diff --git a/chats/apps/discussions/tests/test_discussion_msgs_actions.py b/chats/apps/discussions/tests/test_discussion_msgs_actions.py new file mode 100644 index 00000000..49b561c3 --- /dev/null +++ b/chats/apps/discussions/tests/test_discussion_msgs_actions.py @@ -0,0 +1,111 @@ +from django.urls import reverse +from parameterized import parameterized +from rest_framework import status +from rest_framework.test import APITestCase + + +class CreateDiscussionMessageViewActionTests(APITestCase): + # ("Scenario description", room, queue, subject, initial_message, user_token, expected_response_status) + fixtures = [ + "chats/fixtures/fixture_app.json", + "chats/fixtures/fixture_discussion.json", + ] + + parameters = [ + # Success parameters + ( + "Added user can send messages to the discussion", + "3c2d1694-8db9-4f09-976b-e263f9d79c99", + "super large giant text phrase very cool v2", + "d7fddba0b1dfaad72aa9e21876cbc93caa9ce3fa", + status.HTTP_201_CREATED, + ), + ( + "Outside room user cannot send messages to the discussion", + "3c2d1694-8db9-4f09-976b-e263f9d79c99", + "super large giant text phrase very cool v2", + "a0358e20c8755568189d3a7e688ac3ec771317e2", + status.HTTP_403_FORBIDDEN, + ), + ( + "Outside room admin cannot send messages to the discussion", + "3c2d1694-8db9-4f09-976b-e263f9d79c99", + "super large giant text phrase very cool v2", + "4215e6d6666e54f7db9f98100533aa68909fd855", + status.HTTP_403_FORBIDDEN, + ), + ] + + def _create_discussion_user(self, token, discussion, body): + url = ( + reverse("discussion-detail", kwargs={"uuid": discussion}) + "send_messages/" + ) + client = self.client + client.credentials(HTTP_AUTHORIZATION="Token " + token) + response = client.post(url, format="json", data=body) + return response + + @parameterized.expand(parameters) + def test_send_messages_to_discussion( + self, _, discussion, text, token, expected_status + ): + discussion_data = { + "text": text, + } + + response = self._create_discussion_user(token, discussion, discussion_data) + self.assertEqual(response.status_code, expected_status) + + +class ListDiscussionMsgsViewActionTests(APITestCase): + fixtures = [ + "chats/fixtures/fixture_app.json", + "chats/fixtures/fixture_discussion.json", + ] + parameters = [ + ( + "Creator can list all msgs on the discussions", + "d7fddba0b1dfaad72aa9e21876cbc93caa9ce3fa", + "3c2d1694-8db9-4f09-976b-e263f9d79c99", + status.HTTP_200_OK, + 1, + ), + ( + "Admin can list all msgs on the discussions", + "4215e6d6666e54f7db9f98100533aa68909fd855", + "3c2d1694-8db9-4f09-976b-e263f9d79c99", + status.HTTP_200_OK, + 1, + ), + ( + "Added user can list all msgs on the discussions", + "a0358e20c8755568189d3a7e688ac3ec771317e2", + "36584c70-aaf9-4f5c-b0c3-0547bb23879d", + status.HTTP_200_OK, + 1, + ), + ( + "Outside project user cannot list discussion msgs", + "1218da72b087b8be7f0e2520a515e968ab866fdd", + "3c2d1694-8db9-4f09-976b-e263f9d79c99", + status.HTTP_403_FORBIDDEN, + None, + ), + ] + + def _list_discussion_user(self, token, discussion, params={}): + url = ( + reverse("discussion-detail", kwargs={"uuid": discussion}) + "list_messages/" + ) + client = self.client + client.credentials(HTTP_AUTHORIZATION="Token " + token) + response = client.get(url, data=params) + return response + + @parameterized.expand(parameters) + def test_discussion_msgs( + self, _, token, discussion, expected_status, expected_count + ): + response = self._list_discussion_user(token=token, discussion=discussion) + self.assertEqual(response.status_code, expected_status) + self.assertEqual(response.json().get("count"), expected_count) diff --git a/chats/apps/discussions/usecases/__init__.py b/chats/apps/discussions/usecases/__init__.py index 70996207..8153a206 100644 --- a/chats/apps/discussions/usecases/__init__.py +++ b/chats/apps/discussions/usecases/__init__.py @@ -1 +1,2 @@ from .create_discussion import CreateDiscussionUseCase # noqa +from .create_message_with_media import CreateMessageWithMediaUseCase # noqa diff --git a/chats/apps/discussions/usecases/create_message_with_media.py b/chats/apps/discussions/usecases/create_message_with_media.py new file mode 100644 index 00000000..bdcd3dcf --- /dev/null +++ b/chats/apps/discussions/usecases/create_message_with_media.py @@ -0,0 +1,22 @@ +from ..models import DiscussionMessage + + +class CreateMessageWithMediaUseCase: + def __init__(self, discussion, user, msg_content: dict, notify: bool = True): + self.discussion = discussion + self.user = user + self.msg_content = msg_content + self.notify = notify + + def _create_message(self, text): + return DiscussionMessage.objects.create( + discussion=self.discussion, user=self.user, text=text + ) + + def execute(self): + text = self.msg_content.pop("text", "") + msg = self._create_message(text) + media = msg.medias.create(**self.msg_content) + if self.notify: + msg.notify("create") + return media diff --git a/chats/apps/discussions/views/_discussion_message_actions.py b/chats/apps/discussions/views/_discussion_message_actions.py new file mode 100644 index 00000000..dfa17722 --- /dev/null +++ b/chats/apps/discussions/views/_discussion_message_actions.py @@ -0,0 +1,89 @@ +from rest_framework import parsers, status +from rest_framework.decorators import action +from rest_framework.response import Response + +from ..models import DiscussionMessage +from ..serializers import ( + DiscussionCreateMessageSerializer, + DiscussionReadMessageSerializer, + MessageAndMediaSimpleSerializer, +) +from ..usecases import CreateMessageWithMediaUseCase + + +class DiscussionMessageActionsMixin: + """This should be used with a Discussion model viewset""" + + @action( + detail=True, methods=["POST"], url_name="send_messages", filterset_class=None + ) + def send_messages(self, request, *args, **kwargs): + user = request.user + discussion = self.get_object() + try: + serializer = DiscussionCreateMessageSerializer(data=request.data) + serializer.is_valid(raise_exception=True) + msg = discussion.create_discussion_message( + message=serializer.validated_data.get("text"), user=user + ) + serialized_msg = DiscussionReadMessageSerializer(instance=msg) + + return Response( + serialized_msg.data, + status=status.HTTP_201_CREATED, + ) + except Exception as error: + return Response( + {"detail": f"{type(error)}: {error}"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + @action( + detail=True, methods=["GET"], url_name="list_messages", filterset_class=None + ) + def list_messages(self, request, *args, **kwargs): + discussion = self.get_object() + + queryset = DiscussionMessage.objects.filter(discussion=discussion) + ordering = request.GET.get("ordering") + if ordering: + order_list = ordering.split(",") + + queryset = queryset.order_by(*order_list) + + page = self.paginate_queryset(queryset) + reverse_page = request.GET.get("reverse_results") + if reverse_page: + page.reverse() + + if page is not None: + serializer = DiscussionReadMessageSerializer(page, many=True) + return self.get_paginated_response(serializer.data) + + serializer = DiscussionReadMessageSerializer(queryset, many=True) + return Response(serializer.data) + + @action( + methods=["POST"], + detail=True, + url_name="create_media", + parser_classes=[parsers.MultiPartParser], + ) + def send_media_messages(self, request, *args, **kwargs): + serializer = MessageAndMediaSimpleSerializer(data=request.data) + serializer.is_valid(raise_exception=True) + + discussion = self.get_object() + user = request.user + + message_media = CreateMessageWithMediaUseCase( + discussion, user, serializer.validated_data + ).execute() + serialized_message = DiscussionReadMessageSerializer( + instance=message_media.message + ) + headers = self.get_success_headers(serialized_message.data) + + return Response( + serialized_message.data, status=status.HTTP_201_CREATED, headers=headers + ) diff --git a/chats/apps/discussions/views/discussion.py b/chats/apps/discussions/views/discussion.py index e49cf7f3..061d62d9 100644 --- a/chats/apps/discussions/views/discussion.py +++ b/chats/apps/discussions/views/discussion.py @@ -14,13 +14,16 @@ DiscussionListSerializer, ) from ..usecases import CreateDiscussionUseCase +from ._discussion_message_actions import DiscussionMessageActionsMixin from ._discussion_user_actions import DiscussionUserActionsMixin from .permissions import CanManageDiscussion User = get_user_model() -class DiscussionViewSet(viewsets.ModelViewSet, DiscussionUserActionsMixin): +class DiscussionViewSet( + viewsets.ModelViewSet, DiscussionMessageActionsMixin, DiscussionUserActionsMixin +): queryset = Discussion.objects.all() filter_backends = [filters.OrderingFilter, DjangoFilterBackend] filterset_class = DiscussionFilter