add report chat
This commit is contained in:
parent
e4cab3ab62
commit
c8883832c9
9 changed files with 260 additions and 1 deletions
|
|
@ -1,3 +1,23 @@
|
|||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
||||
from apps.chat.models import ConversationReport
|
||||
|
||||
|
||||
@admin.register(ConversationReport)
|
||||
class ConversationReportAdmin(admin.ModelAdmin):
|
||||
list_display = ("uuid", "conversation", "reporter_uuid", "reason", "created_at")
|
||||
list_filter = ("reason", "created_at")
|
||||
search_fields = ("uuid", "conversation__uuid", "reporter_uuid")
|
||||
readonly_fields = (
|
||||
"uuid",
|
||||
"conversation",
|
||||
"reporter_uuid",
|
||||
"reason",
|
||||
"other_reason_text",
|
||||
"created_at",
|
||||
)
|
||||
ordering = ("-created_at",)
|
||||
|
||||
def has_add_permission(self, request):
|
||||
# Reports are only ever created by users through the API.
|
||||
return False
|
||||
|
|
|
|||
29
apps/chat/migrations/0004_conversationreport.py
Normal file
29
apps/chat/migrations/0004_conversationreport.py
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
# Generated by Django 5.2.13 on 2026-07-20 10:17
|
||||
|
||||
import django.db.models.deletion
|
||||
import uuid
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('chat', '0003_conversation_closed_by_uuid_conversation_status_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='ConversationReport',
|
||||
fields=[
|
||||
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||
('reporter_uuid', models.UUIDField()),
|
||||
('reason', models.CharField(choices=[('harassing_message', 'پیام آزار دهنده'), ('fraud', 'کلاه\u200cبرداری'), ('inappropriate_content', 'محتوای نامناسب'), ('other', 'سایر')], max_length=32)),
|
||||
('other_reason_text', models.TextField(blank=True, null=True)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('conversation', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='reports', to='chat.conversation')),
|
||||
],
|
||||
options={
|
||||
'indexes': [models.Index(fields=['conversation'], name='chat_conver_convers_d48bec_idx'), models.Index(fields=['reporter_uuid'], name='chat_conver_reporte_5dc041_idx'), models.Index(fields=['reason'], name='chat_conver_reason_62cd7f_idx'), models.Index(fields=['created_at'], name='chat_conver_created_6ce88a_idx')],
|
||||
},
|
||||
),
|
||||
]
|
||||
|
|
@ -2,6 +2,7 @@ from apps.chat.models.conversation import Conversation, ConversationStatus, Conv
|
|||
from apps.chat.models.mapping import MattermostAccountMapping
|
||||
from apps.chat.models.participant import ConversationParticipant
|
||||
from apps.chat.models.read_state import ConversationReadState
|
||||
from apps.chat.models.report import ConversationReport, ReportReason
|
||||
|
||||
__all__ = [
|
||||
"Conversation",
|
||||
|
|
@ -10,4 +11,6 @@ __all__ = [
|
|||
"ConversationParticipant",
|
||||
"MattermostAccountMapping",
|
||||
"ConversationReadState",
|
||||
"ConversationReport",
|
||||
"ReportReason",
|
||||
]
|
||||
|
|
|
|||
37
apps/chat/models/report.py
Normal file
37
apps/chat/models/report.py
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import uuid
|
||||
|
||||
from django.db import models
|
||||
|
||||
from apps.chat.models.conversation import Conversation
|
||||
|
||||
|
||||
class ReportReason(models.TextChoices):
|
||||
HARASSING_MESSAGE = "harassing_message", "پیام آزار دهنده"
|
||||
FRAUD = "fraud", "کلاهبرداری"
|
||||
INAPPROPRIATE_CONTENT = "inappropriate_content", "محتوای نامناسب"
|
||||
OTHER = "other", "سایر"
|
||||
|
||||
|
||||
class ConversationReport(models.Model):
|
||||
uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||
conversation = models.ForeignKey(
|
||||
Conversation,
|
||||
related_name="reports",
|
||||
on_delete=models.CASCADE,
|
||||
)
|
||||
reporter_uuid = models.UUIDField()
|
||||
reason = models.CharField(max_length=32, choices=ReportReason.choices)
|
||||
# Only populated when reason == OTHER.
|
||||
other_reason_text = models.TextField(null=True, blank=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
indexes = [
|
||||
models.Index(fields=["conversation"]),
|
||||
models.Index(fields=["reporter_uuid"]),
|
||||
models.Index(fields=["reason"]),
|
||||
models.Index(fields=["created_at"]),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.get_reason_display()} — {self.conversation_id}"
|
||||
31
apps/chat/serializers/reports.py
Normal file
31
apps/chat/serializers/reports.py
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
from rest_framework import serializers
|
||||
|
||||
from apps.chat.models import ConversationReport, ReportReason
|
||||
|
||||
|
||||
class CreateReportSerializer(serializers.Serializer):
|
||||
reporter_uuid = serializers.UUIDField()
|
||||
reason = serializers.ChoiceField(choices=ReportReason.choices)
|
||||
other_reason_text = serializers.CharField(required=False, allow_null=True, allow_blank=True)
|
||||
|
||||
def validate(self, attrs):
|
||||
if attrs["reason"] == ReportReason.OTHER and not attrs.get("other_reason_text"):
|
||||
raise serializers.ValidationError(
|
||||
{"other_reason_text": "other_reason_text is required when reason is 'other'."}
|
||||
)
|
||||
return attrs
|
||||
|
||||
|
||||
class ReportSerializer(serializers.ModelSerializer):
|
||||
conversation_uuid = serializers.UUIDField(source="conversation_id")
|
||||
|
||||
class Meta:
|
||||
model = ConversationReport
|
||||
fields = [
|
||||
"uuid",
|
||||
"conversation_uuid",
|
||||
"reporter_uuid",
|
||||
"reason",
|
||||
"other_reason_text",
|
||||
"created_at",
|
||||
]
|
||||
20
apps/chat/services/report.py
Normal file
20
apps/chat/services/report.py
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
from uuid import UUID
|
||||
|
||||
from apps.chat.models import Conversation, ConversationReport, ReportReason
|
||||
|
||||
|
||||
class ReportService:
|
||||
def create(
|
||||
self,
|
||||
conversation_uuid: UUID,
|
||||
reporter_uuid: UUID,
|
||||
reason: str,
|
||||
other_reason_text: str | None = None,
|
||||
) -> ConversationReport:
|
||||
conversation = Conversation.objects.get(uuid=conversation_uuid)
|
||||
return ConversationReport.objects.create(
|
||||
conversation=conversation,
|
||||
reporter_uuid=reporter_uuid,
|
||||
reason=reason,
|
||||
other_reason_text=other_reason_text if reason == ReportReason.OTHER else None,
|
||||
)
|
||||
85
apps/chat/tests/test_reports.py
Normal file
85
apps/chat/tests/test_reports.py
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
import uuid
|
||||
|
||||
import pytest
|
||||
|
||||
from apps.chat.models import Conversation, ConversationReport, ReportReason
|
||||
from apps.chat.serializers.reports import CreateReportSerializer
|
||||
from apps.chat.services.report import ReportService
|
||||
|
||||
|
||||
def _make_conv(channel_id="ch-report"):
|
||||
return Conversation.objects.create(mattermost_channel_id=channel_id)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_create_report_with_predefined_reason():
|
||||
conv = _make_conv()
|
||||
reporter_uuid = uuid.uuid4()
|
||||
|
||||
report = ReportService().create(
|
||||
conv.uuid, reporter_uuid, ReportReason.FRAUD
|
||||
)
|
||||
|
||||
assert report.conversation_id == conv.uuid
|
||||
assert report.reporter_uuid == reporter_uuid
|
||||
assert report.reason == ReportReason.FRAUD
|
||||
assert report.other_reason_text is None
|
||||
assert ConversationReport.objects.filter(uuid=report.uuid).exists()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_create_report_other_reason_stores_text():
|
||||
conv = _make_conv()
|
||||
reporter_uuid = uuid.uuid4()
|
||||
|
||||
report = ReportService().create(
|
||||
conv.uuid, reporter_uuid, ReportReason.OTHER, other_reason_text="spamming me constantly"
|
||||
)
|
||||
|
||||
assert report.reason == ReportReason.OTHER
|
||||
assert report.other_reason_text == "spamming me constantly"
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_create_report_ignores_other_reason_text_for_non_other_reason():
|
||||
conv = _make_conv()
|
||||
reporter_uuid = uuid.uuid4()
|
||||
|
||||
report = ReportService().create(
|
||||
conv.uuid,
|
||||
reporter_uuid,
|
||||
ReportReason.INAPPROPRIATE_CONTENT,
|
||||
other_reason_text="this should be dropped",
|
||||
)
|
||||
|
||||
assert report.reason == ReportReason.INAPPROPRIATE_CONTENT
|
||||
assert report.other_reason_text is None
|
||||
|
||||
|
||||
def test_serializer_requires_other_reason_text_when_reason_is_other():
|
||||
serializer = CreateReportSerializer(
|
||||
data={"reporter_uuid": str(uuid.uuid4()), "reason": ReportReason.OTHER}
|
||||
)
|
||||
|
||||
assert not serializer.is_valid()
|
||||
assert "other_reason_text" in serializer.errors
|
||||
|
||||
|
||||
def test_serializer_valid_without_other_reason_text_for_predefined_reason():
|
||||
serializer = CreateReportSerializer(
|
||||
data={"reporter_uuid": str(uuid.uuid4()), "reason": ReportReason.HARASSING_MESSAGE}
|
||||
)
|
||||
|
||||
assert serializer.is_valid(), serializer.errors
|
||||
|
||||
|
||||
def test_serializer_valid_with_other_reason_text_for_other_reason():
|
||||
serializer = CreateReportSerializer(
|
||||
data={
|
||||
"reporter_uuid": str(uuid.uuid4()),
|
||||
"reason": ReportReason.OTHER,
|
||||
"other_reason_text": "something specific",
|
||||
}
|
||||
)
|
||||
|
||||
assert serializer.is_valid(), serializer.errors
|
||||
|
|
@ -8,6 +8,7 @@ from apps.chat.views.conversations import (
|
|||
)
|
||||
from apps.chat.views.messages import MessageView
|
||||
from apps.chat.views.read_state import ChatEventsView, ReadStateView
|
||||
from apps.chat.views.reports import ConversationReportView
|
||||
|
||||
urlpatterns = [
|
||||
path("api/chats/", ConversationCreateView.as_view(), name="chat-create"),
|
||||
|
|
@ -17,4 +18,5 @@ urlpatterns = [
|
|||
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"),
|
||||
path("api/chats/<uuid:chat_uuid>/report/", ConversationReportView.as_view(), name="chat-report"),
|
||||
]
|
||||
|
|
|
|||
32
apps/chat/views/reports.py
Normal file
32
apps/chat/views/reports.py
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
from uuid import UUID
|
||||
|
||||
from drf_spectacular.utils import extend_schema
|
||||
from rest_framework import status
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from apps.chat.serializers.reports import CreateReportSerializer, ReportSerializer
|
||||
from apps.chat.services.report import ReportService
|
||||
|
||||
|
||||
class ConversationReportView(APIView):
|
||||
authentication_classes = []
|
||||
permission_classes = []
|
||||
|
||||
@extend_schema(
|
||||
request=CreateReportSerializer,
|
||||
responses={201: ReportSerializer},
|
||||
)
|
||||
def post(self, request, chat_uuid):
|
||||
serializer = CreateReportSerializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
data = serializer.validated_data
|
||||
|
||||
report = ReportService().create(
|
||||
conversation_uuid=UUID(str(chat_uuid)),
|
||||
reporter_uuid=data["reporter_uuid"],
|
||||
reason=data["reason"],
|
||||
other_reason_text=data.get("other_reason_text"),
|
||||
)
|
||||
|
||||
return Response(ReportSerializer(report).data, status=status.HTTP_201_CREATED)
|
||||
Loading…
Add table
Reference in a new issue