Skip to content

Commit 678891d

Browse files
committed
adapt back with blocknote interface
1 parent b555d63 commit 678891d

16 files changed

+727
-192
lines changed

src/backend/core/api/serializers.py

Lines changed: 183 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -879,17 +879,24 @@ class MoveDocumentSerializer(serializers.Serializer):
879879
)
880880

881881

882-
class CommentSerializer(serializers.ModelSerializer):
883-
"""Serialize comments."""
882+
class ThreadSerializer(serializers.ModelSerializer):
883+
"""Serialize threads in a backward compatible shape for current frontend.
884+
885+
We expose a flatten representation where ``content`` maps to the first
886+
comment's body. Creating a thread requires a ``content`` field which is
887+
stored as the first comment.
888+
"""
884889

885890
user = UserLightSerializer(read_only=True)
886891
abilities = serializers.SerializerMethodField(read_only=True)
892+
content = serializers.JSONField(write_only=True, required=True)
893+
document = serializers.PrimaryKeyRelatedField(read_only=True)
887894

888895
class Meta:
889-
model = models.Comment
896+
model = models.Thread
890897
fields = [
891898
"id",
892-
"content",
899+
"content", # write: body of initial comment; read: not returned (we override to_representation)
893900
"created_at",
894901
"updated_at",
895902
"user",
@@ -905,19 +912,182 @@ class Meta:
905912
"abilities",
906913
]
907914

908-
def get_abilities(self, comment) -> dict:
909-
"""Return abilities of the logged-in user on the instance."""
915+
def to_representation(self, instance):
916+
rep = super().to_representation(instance)
917+
# Provide first comment body as ``content`` to match previous API.
918+
first_comment = instance.first_comment
919+
rep["content"] = first_comment.body if first_comment else None
920+
return rep
921+
922+
def get_abilities(self, thread) -> dict: # type: ignore[override]
910923
request = self.context.get("request")
911924
if request:
912-
return comment.get_abilities(request.user)
925+
return thread.get_abilities(request.user)
913926
return {}
914927

915-
def validate(self, attrs):
916-
"""Validate invitation data."""
928+
def create(self, validated_data):
917929
request = self.context.get("request")
918-
user = getattr(request, "user", None)
930+
document_id = self.context.get("resource_id")
931+
content = validated_data.pop("content")
932+
document = models.Document.objects.get(pk=document_id)
933+
user = request.user if request else None
934+
thread = models.Thread.objects.create(document=document, user=user)
935+
models.Comment.objects.create(thread=thread, user=user, body=content)
936+
return thread
937+
938+
def update(self, instance, validated_data): # pragma: no cover - not used yet
939+
# Allow updating first comment body for backward compatibility.
940+
content = validated_data.get("content")
941+
if content is not None:
942+
first = instance.first_comment
943+
if first:
944+
first.body = content
945+
first.save(update_fields=["body", "updated_at"])
946+
return instance
947+
948+
949+
class CommentInThreadSerializer(serializers.ModelSerializer):
950+
"""Serialize comments (nested under a thread) with reactions and abilities."""
919951

920-
attrs["document_id"] = self.context["resource_id"]
921-
attrs["user_id"] = user.id if user else None
952+
user = UserLightSerializer(read_only=True)
953+
reactions = serializers.SerializerMethodField()
954+
abilities = serializers.SerializerMethodField()
922955

923-
return attrs
956+
class Meta:
957+
model = models.Comment
958+
fields = [
959+
"id",
960+
"user",
961+
"body",
962+
"created_at",
963+
"updated_at",
964+
"reactions",
965+
"abilities",
966+
]
967+
read_only_fields = fields
968+
969+
def get_reactions(self, obj):
970+
# Collect all users for reactions in a single query
971+
from django.contrib.auth import get_user_model
972+
User = get_user_model()
973+
reactions = list(obj.reactions.all())
974+
user_ids = set()
975+
for r in reactions:
976+
user_ids.update(r.user_ids or [])
977+
users_by_id = {
978+
u.id: u
979+
for u in User.objects.filter(id__in=user_ids).only(
980+
"id", "email", "full_name", "short_name", "language"
981+
)
982+
}
983+
# Serialize users with UserLightSerializer semantics (full_name/short_name logic)
984+
user_serializer = UserLightSerializer
985+
return [
986+
{
987+
"emoji": r.emoji,
988+
"created_at": r.created_at,
989+
"users": [
990+
user_serializer(users_by_id[uid]).data
991+
for uid in r.user_ids
992+
if uid in users_by_id
993+
],
994+
}
995+
for r in reactions
996+
]
997+
998+
def get_abilities(self, obj):
999+
request = self.context.get("request")
1000+
if request:
1001+
return obj.get_abilities(request.user)
1002+
return {}
1003+
1004+
1005+
class ThreadFullSerializer(serializers.ModelSerializer):
1006+
"""Full thread representation with nested comments."""
1007+
1008+
user = UserLightSerializer(read_only=True)
1009+
comments = serializers.SerializerMethodField()
1010+
abilities = serializers.SerializerMethodField()
1011+
1012+
class Meta:
1013+
model = models.Thread
1014+
fields = [
1015+
"id",
1016+
"created_at",
1017+
"updated_at",
1018+
"user",
1019+
"resolved",
1020+
"resolved_updated_at",
1021+
"resolved_by",
1022+
"metadata",
1023+
"comments",
1024+
"abilities",
1025+
]
1026+
read_only_fields = fields
1027+
1028+
def get_comments(self, instance):
1029+
qs = instance.comments.select_related("user").prefetch_related("reactions")
1030+
return CommentInThreadSerializer(qs, many=True, context=self.context).data
1031+
1032+
def get_abilities(self, instance):
1033+
request = self.context.get("request")
1034+
if request:
1035+
return instance.get_abilities(request.user)
1036+
return {}
1037+
1038+
1039+
class CreateThreadSerializer(serializers.Serializer):
1040+
body = serializers.JSONField(required=True)
1041+
metadata = serializers.JSONField(required=False)
1042+
1043+
def create(self, validated_data):
1044+
request = self.context.get("request")
1045+
document = self.context.get("document")
1046+
thread = models.Thread.objects.create(
1047+
document=document,
1048+
user=request.user if request else None,
1049+
metadata=validated_data.get("metadata", {}),
1050+
)
1051+
models.Comment.objects.create(
1052+
thread=thread,
1053+
user=request.user if request else None,
1054+
body=validated_data["body"],
1055+
metadata=validated_data.get("metadata", {}),
1056+
)
1057+
return thread
1058+
1059+
1060+
class CreateCommentSerializer(serializers.Serializer):
1061+
body = serializers.JSONField(required=True)
1062+
metadata = serializers.JSONField(required=False)
1063+
1064+
def create(self, validated_data):
1065+
request = self.context.get("request")
1066+
thread = self.context.get("thread")
1067+
return models.Comment.objects.create(
1068+
thread=thread,
1069+
user=request.user if request else None,
1070+
body=validated_data["body"],
1071+
metadata=validated_data.get("metadata", {}),
1072+
)
1073+
1074+
1075+
class UpdateCommentSerializer(serializers.ModelSerializer):
1076+
class Meta:
1077+
model = models.Comment
1078+
fields = ["body"]
1079+
1080+
1081+
class ReactionCreateSerializer(serializers.Serializer):
1082+
emoji = serializers.CharField(max_length=32)
1083+
1084+
def save(self, **kwargs): # pylint: disable=unused-argument
1085+
request = self.context.get("request")
1086+
comment = self.context.get("comment")
1087+
emoji = self.validated_data["emoji"]
1088+
reaction, _created = models.Reaction.objects.get_or_create(
1089+
comment=comment, emoji=emoji
1090+
)
1091+
if request and request.user:
1092+
reaction.add_user(request.user)
1093+
return reaction

0 commit comments

Comments
 (0)