@@ -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