feature/refactor_uuid #1
11 changed files with 255 additions and 12 deletions
2
apps/chat/exceptions.py
Normal file
2
apps/chat/exceptions.py
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
class ConversationClosedError(Exception):
|
||||||
|
"""Raised when attempting to send a message to a closed conversation."""
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
# Generated by Django 5.2.13 on 2026-07-19 09:05
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('chat', '0002_remove_conversationparticipant_unique_participant_per_conversation_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='conversation',
|
||||||
|
name='closed_by_uuid',
|
||||||
|
field=models.UUIDField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='conversation',
|
||||||
|
name='status',
|
||||||
|
field=models.CharField(choices=[('open', 'Open'), ('closed', 'Closed')], default='open', max_length=20),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name='conversation',
|
||||||
|
index=models.Index(fields=['status'], name='chat_conver_status_9f0685_idx'),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
from apps.chat.models.conversation import Conversation, ConversationType
|
from apps.chat.models.conversation import Conversation, ConversationStatus, ConversationType
|
||||||
from apps.chat.models.mapping import MattermostAccountMapping
|
from apps.chat.models.mapping import MattermostAccountMapping
|
||||||
from apps.chat.models.participant import ConversationParticipant
|
from apps.chat.models.participant import ConversationParticipant
|
||||||
from apps.chat.models.read_state import ConversationReadState
|
from apps.chat.models.read_state import ConversationReadState
|
||||||
|
|
@ -6,6 +6,7 @@ from apps.chat.models.read_state import ConversationReadState
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"Conversation",
|
"Conversation",
|
||||||
"ConversationType",
|
"ConversationType",
|
||||||
|
"ConversationStatus",
|
||||||
"ConversationParticipant",
|
"ConversationParticipant",
|
||||||
"MattermostAccountMapping",
|
"MattermostAccountMapping",
|
||||||
"ConversationReadState",
|
"ConversationReadState",
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,11 @@ class ConversationType(models.TextChoices):
|
||||||
GROUP = "group", "Group"
|
GROUP = "group", "Group"
|
||||||
|
|
||||||
|
|
||||||
|
class ConversationStatus(models.TextChoices):
|
||||||
|
OPEN = "open", "Open"
|
||||||
|
CLOSED = "closed", "Closed"
|
||||||
|
|
||||||
|
|
||||||
class Conversation(models.Model):
|
class Conversation(models.Model):
|
||||||
uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||||
# Internal only — never exposed through any public API or serializer
|
# Internal only — never exposed through any public API or serializer
|
||||||
|
|
@ -17,6 +22,13 @@ class Conversation(models.Model):
|
||||||
choices=ConversationType.choices,
|
choices=ConversationType.choices,
|
||||||
default=ConversationType.DIRECT,
|
default=ConversationType.DIRECT,
|
||||||
)
|
)
|
||||||
|
status = models.CharField(
|
||||||
|
max_length=20,
|
||||||
|
choices=ConversationStatus.choices,
|
||||||
|
default=ConversationStatus.OPEN,
|
||||||
|
)
|
||||||
|
# Set only while status is CLOSED; identifies who is allowed to reopen it.
|
||||||
|
closed_by_uuid = models.UUIDField(null=True, blank=True)
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
updated_at = models.DateTimeField(auto_now=True)
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
|
|
@ -24,6 +36,7 @@ class Conversation(models.Model):
|
||||||
indexes = [
|
indexes = [
|
||||||
models.Index(fields=["type"]),
|
models.Index(fields=["type"]),
|
||||||
models.Index(fields=["created_at"]),
|
models.Index(fields=["created_at"]),
|
||||||
|
models.Index(fields=["status"]),
|
||||||
]
|
]
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
|
|
|
||||||
|
|
@ -9,12 +9,16 @@ class CreateConversationSerializer(serializers.Serializer):
|
||||||
user_2_uuid = serializers.UUIDField()
|
user_2_uuid = serializers.UUIDField()
|
||||||
|
|
||||||
|
|
||||||
|
class ConversationUserActionSerializer(serializers.Serializer):
|
||||||
|
user_uuid = serializers.UUIDField()
|
||||||
|
|
||||||
|
|
||||||
class ConversationSerializer(serializers.ModelSerializer):
|
class ConversationSerializer(serializers.ModelSerializer):
|
||||||
participants = serializers.SerializerMethodField()
|
participants = serializers.SerializerMethodField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Conversation
|
model = Conversation
|
||||||
fields = ["uuid", "type", "created_at", "participants"]
|
fields = ["uuid", "type", "status", "closed_by_uuid", "created_at", "participants"]
|
||||||
|
|
||||||
@extend_schema_field(serializers.ListField(child=serializers.UUIDField()))
|
@extend_schema_field(serializers.ListField(child=serializers.UUIDField()))
|
||||||
def get_participants(self, obj):
|
def get_participants(self, obj):
|
||||||
|
|
@ -28,6 +32,8 @@ class ConversationSerializer(serializers.ModelSerializer):
|
||||||
class ConversationListSerializer(serializers.Serializer):
|
class ConversationListSerializer(serializers.Serializer):
|
||||||
uuid = serializers.UUIDField()
|
uuid = serializers.UUIDField()
|
||||||
type = serializers.CharField()
|
type = serializers.CharField()
|
||||||
|
status = serializers.CharField()
|
||||||
|
closed_by_uuid = serializers.UUIDField(allow_null=True)
|
||||||
created_at = serializers.DateTimeField()
|
created_at = serializers.DateTimeField()
|
||||||
participants = serializers.ListField(child=serializers.UUIDField())
|
participants = serializers.ListField(child=serializers.UUIDField())
|
||||||
has_unread = serializers.BooleanField()
|
has_unread = serializers.BooleanField()
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ from apps.chat.integrations.mattermost.client import MattermostClient
|
||||||
from apps.chat.models import (
|
from apps.chat.models import (
|
||||||
Conversation,
|
Conversation,
|
||||||
ConversationParticipant,
|
ConversationParticipant,
|
||||||
|
ConversationStatus,
|
||||||
MattermostAccountMapping,
|
MattermostAccountMapping,
|
||||||
)
|
)
|
||||||
from apps.chat.services.account import AccountService
|
from apps.chat.services.account import AccountService
|
||||||
|
|
@ -63,3 +64,28 @@ class ConversationService:
|
||||||
"-created_at"
|
"-created_at"
|
||||||
)[offset : offset + page_size]
|
)[offset : offset + page_size]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def close(self, conversation_uuid: UUID, user_uuid: UUID) -> Conversation:
|
||||||
|
conversation = Conversation.objects.get(uuid=conversation_uuid)
|
||||||
|
if conversation.status == ConversationStatus.CLOSED:
|
||||||
|
return conversation
|
||||||
|
|
||||||
|
conversation.status = ConversationStatus.CLOSED
|
||||||
|
conversation.closed_by_uuid = user_uuid
|
||||||
|
conversation.save(update_fields=["status", "closed_by_uuid", "updated_at"])
|
||||||
|
return conversation
|
||||||
|
|
||||||
|
def reopen(self, conversation_uuid: UUID, user_uuid: UUID) -> Conversation:
|
||||||
|
conversation = Conversation.objects.get(uuid=conversation_uuid)
|
||||||
|
if conversation.status == ConversationStatus.OPEN:
|
||||||
|
return conversation
|
||||||
|
|
||||||
|
if conversation.closed_by_uuid != user_uuid:
|
||||||
|
raise PermissionError(
|
||||||
|
"Only the user who closed this conversation can reopen it"
|
||||||
|
)
|
||||||
|
|
||||||
|
conversation.status = ConversationStatus.OPEN
|
||||||
|
conversation.closed_by_uuid = None
|
||||||
|
conversation.save(update_fields=["status", "closed_by_uuid", "updated_at"])
|
||||||
|
return conversation
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,9 @@ from apps.chat.events.event import MessageSentEvent
|
||||||
from apps.chat.events.publishers.longpoll import LongPollPublisher
|
from apps.chat.events.publishers.longpoll import LongPollPublisher
|
||||||
from apps.chat.events.publishers.push import PushPublisher
|
from apps.chat.events.publishers.push import PushPublisher
|
||||||
from apps.chat.events.publishers.websocket import WebSocketPublisher
|
from apps.chat.events.publishers.websocket import WebSocketPublisher
|
||||||
|
from apps.chat.exceptions import ConversationClosedError
|
||||||
from apps.chat.integrations.mattermost.client import MattermostClient
|
from apps.chat.integrations.mattermost.client import MattermostClient
|
||||||
from apps.chat.models import Conversation, MattermostAccountMapping
|
from apps.chat.models import Conversation, ConversationStatus, MattermostAccountMapping
|
||||||
from apps.chat.services.account import AccountService
|
from apps.chat.services.account import AccountService
|
||||||
from apps.chat.services.storage import StorageService
|
from apps.chat.services.storage import StorageService
|
||||||
|
|
||||||
|
|
@ -42,6 +43,10 @@ class MessageService:
|
||||||
text: str | None = None,
|
text: str | None = None,
|
||||||
file=None,
|
file=None,
|
||||||
) -> dict:
|
) -> dict:
|
||||||
|
conversation = Conversation.objects.get(uuid=conversation_uuid)
|
||||||
|
if conversation.status == ConversationStatus.CLOSED:
|
||||||
|
raise ConversationClosedError(f"Conversation {conversation_uuid} is closed")
|
||||||
|
|
||||||
if not self._account.validate_user(sender_uuid):
|
if not self._account.validate_user(sender_uuid):
|
||||||
raise ValueError(f"Sender {sender_uuid} is not valid")
|
raise ValueError(f"Sender {sender_uuid} is not valid")
|
||||||
|
|
||||||
|
|
@ -55,7 +60,6 @@ class MessageService:
|
||||||
else:
|
else:
|
||||||
mm_message = file_url or text or ""
|
mm_message = file_url or text or ""
|
||||||
|
|
||||||
conversation = Conversation.objects.get(uuid=conversation_uuid)
|
|
||||||
post_id = self._mm.post_message(conversation.mattermost_channel_id, mm_message)
|
post_id = self._mm.post_message(conversation.mattermost_channel_id, mm_message)
|
||||||
|
|
||||||
event = MessageSentEvent(
|
event = MessageSentEvent(
|
||||||
|
|
|
||||||
112
apps/chat/tests/test_conversation_close.py
Normal file
112
apps/chat/tests/test_conversation_close.py
Normal file
|
|
@ -0,0 +1,112 @@
|
||||||
|
import uuid
|
||||||
|
from unittest.mock import Mock
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from apps.chat.exceptions import ConversationClosedError
|
||||||
|
from apps.chat.models import Conversation, ConversationStatus
|
||||||
|
from apps.chat.services.conversation import ConversationService
|
||||||
|
from apps.chat.services.message import MessageService
|
||||||
|
|
||||||
|
|
||||||
|
def _make_conv(status=ConversationStatus.OPEN, closed_by_uuid=None, channel_id="ch-close"):
|
||||||
|
return Conversation.objects.create(
|
||||||
|
mattermost_channel_id=channel_id,
|
||||||
|
status=status,
|
||||||
|
closed_by_uuid=closed_by_uuid,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _svc():
|
||||||
|
return ConversationService(mattermost_client=Mock())
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_close_sets_status_and_closed_by():
|
||||||
|
conv = _make_conv()
|
||||||
|
closer = uuid.uuid4()
|
||||||
|
|
||||||
|
result = _svc().close(conv.uuid, closer)
|
||||||
|
|
||||||
|
assert result.status == ConversationStatus.CLOSED
|
||||||
|
assert result.closed_by_uuid == closer
|
||||||
|
|
||||||
|
conv.refresh_from_db()
|
||||||
|
assert conv.status == ConversationStatus.CLOSED
|
||||||
|
assert conv.closed_by_uuid == closer
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_close_is_idempotent():
|
||||||
|
closer = uuid.uuid4()
|
||||||
|
other = uuid.uuid4()
|
||||||
|
conv = _make_conv(status=ConversationStatus.CLOSED, closed_by_uuid=closer)
|
||||||
|
|
||||||
|
result = _svc().close(conv.uuid, other)
|
||||||
|
|
||||||
|
assert result.status == ConversationStatus.CLOSED
|
||||||
|
assert result.closed_by_uuid == closer # unchanged — original closer preserved
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_reopen_by_closer_succeeds():
|
||||||
|
closer = uuid.uuid4()
|
||||||
|
conv = _make_conv(status=ConversationStatus.CLOSED, closed_by_uuid=closer)
|
||||||
|
|
||||||
|
result = _svc().reopen(conv.uuid, closer)
|
||||||
|
|
||||||
|
assert result.status == ConversationStatus.OPEN
|
||||||
|
assert result.closed_by_uuid is None
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_reopen_by_other_user_raises_permission_error():
|
||||||
|
closer = uuid.uuid4()
|
||||||
|
other = uuid.uuid4()
|
||||||
|
conv = _make_conv(status=ConversationStatus.CLOSED, closed_by_uuid=closer)
|
||||||
|
|
||||||
|
with pytest.raises(PermissionError):
|
||||||
|
_svc().reopen(conv.uuid, other)
|
||||||
|
|
||||||
|
conv.refresh_from_db()
|
||||||
|
assert conv.status == ConversationStatus.CLOSED
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_reopen_is_idempotent_when_already_open():
|
||||||
|
conv = _make_conv(status=ConversationStatus.OPEN)
|
||||||
|
someone = uuid.uuid4()
|
||||||
|
|
||||||
|
result = _svc().reopen(conv.uuid, someone)
|
||||||
|
|
||||||
|
assert result.status == ConversationStatus.OPEN
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_send_message_to_closed_conversation_raises():
|
||||||
|
closer = uuid.uuid4()
|
||||||
|
conv = _make_conv(status=ConversationStatus.CLOSED, closed_by_uuid=closer)
|
||||||
|
sender_uuid = uuid.uuid4()
|
||||||
|
|
||||||
|
svc = MessageService(mattermost_client=Mock(), storage_service=Mock(), publishers=[])
|
||||||
|
|
||||||
|
with pytest.raises(ConversationClosedError):
|
||||||
|
svc.send(conv.uuid, sender_uuid, "text", text="hello")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_send_message_after_reopen_succeeds():
|
||||||
|
closer = uuid.uuid4()
|
||||||
|
conv = _make_conv(status=ConversationStatus.CLOSED, closed_by_uuid=closer)
|
||||||
|
sender_uuid = uuid.uuid4()
|
||||||
|
|
||||||
|
_svc().reopen(conv.uuid, closer)
|
||||||
|
|
||||||
|
mm_client = Mock()
|
||||||
|
mm_client.post_message.return_value = "post-1"
|
||||||
|
svc = MessageService(mattermost_client=mm_client, storage_service=Mock(), publishers=[])
|
||||||
|
|
||||||
|
result = svc.send(conv.uuid, sender_uuid, "text", text="hello again")
|
||||||
|
|
||||||
|
assert result["post_id"] == "post-1"
|
||||||
|
mm_client.post_message.assert_called_once_with("ch-close", "hello again")
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
from django.urls import path
|
from django.urls import path
|
||||||
|
|
||||||
from apps.chat.views.conversations import (
|
from apps.chat.views.conversations import (
|
||||||
|
ConversationCloseView,
|
||||||
ConversationCreateView,
|
ConversationCreateView,
|
||||||
|
ConversationReopenView,
|
||||||
UserConversationListView,
|
UserConversationListView,
|
||||||
)
|
)
|
||||||
from apps.chat.views.messages import MessageView
|
from apps.chat.views.messages import MessageView
|
||||||
|
|
@ -13,4 +15,6 @@ urlpatterns = [
|
||||||
path("api/chats/<uuid:chat_uuid>/messages/", MessageView.as_view(), name="chat-messages"),
|
path("api/chats/<uuid:chat_uuid>/messages/", MessageView.as_view(), name="chat-messages"),
|
||||||
path("api/chats/<uuid:chat_uuid>/read/", ReadStateView.as_view(), name="chat-read"),
|
path("api/chats/<uuid:chat_uuid>/read/", ReadStateView.as_view(), name="chat-read"),
|
||||||
path("api/chats/<uuid:chat_uuid>/events/", ChatEventsView.as_view(), name="chat-events"),
|
path("api/chats/<uuid:chat_uuid>/events/", ChatEventsView.as_view(), name="chat-events"),
|
||||||
|
path("api/chats/<uuid:chat_uuid>/close/", ConversationCloseView.as_view(), name="chat-close"),
|
||||||
|
path("api/chats/<uuid:chat_uuid>/reopen/", ConversationReopenView.as_view(), name="chat-reopen"),
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ from apps.chat.models import ConversationParticipant
|
||||||
from apps.chat.serializers.conversations import (
|
from apps.chat.serializers.conversations import (
|
||||||
ConversationListSerializer,
|
ConversationListSerializer,
|
||||||
ConversationSerializer,
|
ConversationSerializer,
|
||||||
|
ConversationUserActionSerializer,
|
||||||
CreateConversationSerializer,
|
CreateConversationSerializer,
|
||||||
)
|
)
|
||||||
from apps.chat.services.conversation import ConversationService
|
from apps.chat.services.conversation import ConversationService
|
||||||
|
|
@ -84,6 +85,8 @@ class UserConversationListView(APIView):
|
||||||
{
|
{
|
||||||
"uuid": conv.uuid,
|
"uuid": conv.uuid,
|
||||||
"type": conv.type,
|
"type": conv.type,
|
||||||
|
"status": conv.status,
|
||||||
|
"closed_by_uuid": conv.closed_by_uuid,
|
||||||
"created_at": conv.created_at,
|
"created_at": conv.created_at,
|
||||||
"participants": participants,
|
"participants": participants,
|
||||||
"has_unread": has_unread,
|
"has_unread": has_unread,
|
||||||
|
|
@ -92,3 +95,44 @@ class UserConversationListView(APIView):
|
||||||
)
|
)
|
||||||
|
|
||||||
return Response(ConversationListSerializer(result, many=True).data)
|
return Response(ConversationListSerializer(result, many=True).data)
|
||||||
|
|
||||||
|
|
||||||
|
class ConversationCloseView(APIView):
|
||||||
|
authentication_classes = []
|
||||||
|
permission_classes = []
|
||||||
|
|
||||||
|
@extend_schema(
|
||||||
|
request=ConversationUserActionSerializer,
|
||||||
|
responses={200: ConversationSerializer},
|
||||||
|
)
|
||||||
|
def post(self, request, chat_uuid):
|
||||||
|
serializer = ConversationUserActionSerializer(data=request.data)
|
||||||
|
serializer.is_valid(raise_exception=True)
|
||||||
|
|
||||||
|
conversation = ConversationService().close(
|
||||||
|
UUID(str(chat_uuid)), serializer.validated_data["user_uuid"]
|
||||||
|
)
|
||||||
|
|
||||||
|
return Response(ConversationSerializer(conversation).data)
|
||||||
|
|
||||||
|
|
||||||
|
class ConversationReopenView(APIView):
|
||||||
|
authentication_classes = []
|
||||||
|
permission_classes = []
|
||||||
|
|
||||||
|
@extend_schema(
|
||||||
|
request=ConversationUserActionSerializer,
|
||||||
|
responses={200: ConversationSerializer, 403: None},
|
||||||
|
)
|
||||||
|
def post(self, request, chat_uuid):
|
||||||
|
serializer = ConversationUserActionSerializer(data=request.data)
|
||||||
|
serializer.is_valid(raise_exception=True)
|
||||||
|
|
||||||
|
try:
|
||||||
|
conversation = ConversationService().reopen(
|
||||||
|
UUID(str(chat_uuid)), serializer.validated_data["user_uuid"]
|
||||||
|
)
|
||||||
|
except PermissionError as exc:
|
||||||
|
return Response({"detail": str(exc)}, status=status.HTTP_403_FORBIDDEN)
|
||||||
|
|
||||||
|
return Response(ConversationSerializer(conversation).data)
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ from rest_framework import status
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.views import APIView
|
from rest_framework.views import APIView
|
||||||
|
|
||||||
|
from apps.chat.exceptions import ConversationClosedError
|
||||||
from apps.chat.serializers.messages import MessageSerializer, SendMessageSerializer
|
from apps.chat.serializers.messages import MessageSerializer, SendMessageSerializer
|
||||||
from apps.chat.services.message import MessageService
|
from apps.chat.services.message import MessageService
|
||||||
|
|
||||||
|
|
@ -15,13 +16,14 @@ class MessageView(APIView):
|
||||||
|
|
||||||
@extend_schema(
|
@extend_schema(
|
||||||
request=SendMessageSerializer,
|
request=SendMessageSerializer,
|
||||||
responses={201: MessageSerializer},
|
responses={201: MessageSerializer, 403: None},
|
||||||
)
|
)
|
||||||
def post(self, request, chat_uuid):
|
def post(self, request, chat_uuid):
|
||||||
serializer = SendMessageSerializer(data=request.data)
|
serializer = SendMessageSerializer(data=request.data)
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
data = serializer.validated_data
|
data = serializer.validated_data
|
||||||
|
|
||||||
|
try:
|
||||||
result = MessageService().send(
|
result = MessageService().send(
|
||||||
conversation_uuid=UUID(str(chat_uuid)),
|
conversation_uuid=UUID(str(chat_uuid)),
|
||||||
sender_uuid=data["sender_uuid"],
|
sender_uuid=data["sender_uuid"],
|
||||||
|
|
@ -29,6 +31,8 @@ class MessageView(APIView):
|
||||||
text=data.get("text"),
|
text=data.get("text"),
|
||||||
file=data.get("file"),
|
file=data.get("file"),
|
||||||
)
|
)
|
||||||
|
except ConversationClosedError as exc:
|
||||||
|
return Response({"detail": str(exc)}, status=status.HTTP_403_FORBIDDEN)
|
||||||
|
|
||||||
return Response(MessageSerializer(result).data, status=status.HTTP_201_CREATED)
|
return Response(MessageSerializer(result).data, status=status.HTTP_201_CREATED)
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue