@@ -823,17 +823,24 @@ class MoveDocumentSerializer(serializers.Serializer):
823823 )
824824
825825
826- class CommentSerializer (serializers .ModelSerializer ):
827- """Serialize comments."""
826+ class ThreadSerializer (serializers .ModelSerializer ):
827+ """Serialize threads in a backward compatible shape for current frontend.
828+
829+ We expose a flatten representation where ``content`` maps to the first
830+ comment's body. Creating a thread requires a ``content`` field which is
831+ stored as the first comment.
832+ """
828833
829834 user = UserLightSerializer (read_only = True )
830835 abilities = serializers .SerializerMethodField (read_only = True )
836+ content = serializers .JSONField (write_only = True , required = True )
837+ document = serializers .PrimaryKeyRelatedField (read_only = True )
831838
832839 class Meta :
833- model = models .Comment
840+ model = models .Thread
834841 fields = [
835842 "id" ,
836- "content" ,
843+ "content" , # write: body of initial comment; read: not returned (we override to_representation)
837844 "created_at" ,
838845 "updated_at" ,
839846 "user" ,
@@ -849,19 +856,182 @@ class Meta:
849856 "abilities" ,
850857 ]
851858
852- def get_abilities (self , comment ) -> dict :
853- """Return abilities of the logged-in user on the instance."""
859+ def to_representation (self , instance ):
860+ rep = super ().to_representation (instance )
861+ # Provide first comment body as ``content`` to match previous API.
862+ first_comment = instance .first_comment
863+ rep ["content" ] = first_comment .body if first_comment else None
864+ return rep
865+
866+ def get_abilities (self , thread ) -> dict : # type: ignore[override]
854867 request = self .context .get ("request" )
855868 if request :
856- return comment .get_abilities (request .user )
869+ return thread .get_abilities (request .user )
857870 return {}
858871
859- def validate (self , attrs ):
860- """Validate invitation data."""
872+ def create (self , validated_data ):
861873 request = self .context .get ("request" )
862- user = getattr (request , "user" , None )
874+ document_id = self .context .get ("resource_id" )
875+ content = validated_data .pop ("content" )
876+ document = models .Document .objects .get (pk = document_id )
877+ user = request .user if request else None
878+ thread = models .Thread .objects .create (document = document , user = user )
879+ models .Comment .objects .create (thread = thread , user = user , body = content )
880+ return thread
881+
882+ def update (self , instance , validated_data ): # pragma: no cover - not used yet
883+ # Allow updating first comment body for backward compatibility.
884+ content = validated_data .get ("content" )
885+ if content is not None :
886+ first = instance .first_comment
887+ if first :
888+ first .body = content
889+ first .save (update_fields = ["body" , "updated_at" ])
890+ return instance
891+
892+
893+ class CommentInThreadSerializer (serializers .ModelSerializer ):
894+ """Serialize comments (nested under a thread) with reactions and abilities."""
863895
864- attrs ["document_id" ] = self .context ["resource_id" ]
865- attrs ["user_id" ] = user .id if user else None
896+ user = UserLightSerializer (read_only = True )
897+ reactions = serializers .SerializerMethodField ()
898+ abilities = serializers .SerializerMethodField ()
866899
867- return attrs
900+ class Meta :
901+ model = models .Comment
902+ fields = [
903+ "id" ,
904+ "user" ,
905+ "body" ,
906+ "created_at" ,
907+ "updated_at" ,
908+ "reactions" ,
909+ "abilities" ,
910+ ]
911+ read_only_fields = fields
912+
913+ def get_reactions (self , obj ):
914+ # Collect all users for reactions in a single query
915+ from django .contrib .auth import get_user_model
916+ User = get_user_model ()
917+ reactions = list (obj .reactions .all ())
918+ user_ids = set ()
919+ for r in reactions :
920+ user_ids .update (r .user_ids or [])
921+ users_by_id = {
922+ u .id : u
923+ for u in User .objects .filter (id__in = user_ids ).only (
924+ "id" , "email" , "full_name" , "short_name" , "language"
925+ )
926+ }
927+ # Serialize users with UserLightSerializer semantics (full_name/short_name logic)
928+ user_serializer = UserLightSerializer
929+ return [
930+ {
931+ "emoji" : r .emoji ,
932+ "created_at" : r .created_at ,
933+ "users" : [
934+ user_serializer (users_by_id [uid ]).data
935+ for uid in r .user_ids
936+ if uid in users_by_id
937+ ],
938+ }
939+ for r in reactions
940+ ]
941+
942+ def get_abilities (self , obj ):
943+ request = self .context .get ("request" )
944+ if request :
945+ return obj .get_abilities (request .user )
946+ return {}
947+
948+
949+ class ThreadFullSerializer (serializers .ModelSerializer ):
950+ """Full thread representation with nested comments."""
951+
952+ user = UserLightSerializer (read_only = True )
953+ comments = serializers .SerializerMethodField ()
954+ abilities = serializers .SerializerMethodField ()
955+
956+ class Meta :
957+ model = models .Thread
958+ fields = [
959+ "id" ,
960+ "created_at" ,
961+ "updated_at" ,
962+ "user" ,
963+ "resolved" ,
964+ "resolved_updated_at" ,
965+ "resolved_by" ,
966+ "metadata" ,
967+ "comments" ,
968+ "abilities" ,
969+ ]
970+ read_only_fields = fields
971+
972+ def get_comments (self , instance ):
973+ qs = instance .comments .select_related ("user" ).prefetch_related ("reactions" )
974+ return CommentInThreadSerializer (qs , many = True , context = self .context ).data
975+
976+ def get_abilities (self , instance ):
977+ request = self .context .get ("request" )
978+ if request :
979+ return instance .get_abilities (request .user )
980+ return {}
981+
982+
983+ class CreateThreadSerializer (serializers .Serializer ):
984+ body = serializers .JSONField (required = True )
985+ metadata = serializers .JSONField (required = False )
986+
987+ def create (self , validated_data ):
988+ request = self .context .get ("request" )
989+ document = self .context .get ("document" )
990+ thread = models .Thread .objects .create (
991+ document = document ,
992+ user = request .user if request else None ,
993+ metadata = validated_data .get ("metadata" , {}),
994+ )
995+ models .Comment .objects .create (
996+ thread = thread ,
997+ user = request .user if request else None ,
998+ body = validated_data ["body" ],
999+ metadata = validated_data .get ("metadata" , {}),
1000+ )
1001+ return thread
1002+
1003+
1004+ class CreateCommentSerializer (serializers .Serializer ):
1005+ body = serializers .JSONField (required = True )
1006+ metadata = serializers .JSONField (required = False )
1007+
1008+ def create (self , validated_data ):
1009+ request = self .context .get ("request" )
1010+ thread = self .context .get ("thread" )
1011+ return models .Comment .objects .create (
1012+ thread = thread ,
1013+ user = request .user if request else None ,
1014+ body = validated_data ["body" ],
1015+ metadata = validated_data .get ("metadata" , {}),
1016+ )
1017+
1018+
1019+ class UpdateCommentSerializer (serializers .ModelSerializer ):
1020+ class Meta :
1021+ model = models .Comment
1022+ fields = ["body" ]
1023+
1024+
1025+ class ReactionCreateSerializer (serializers .Serializer ):
1026+ emoji = serializers .CharField (max_length = 32 )
1027+
1028+ def save (self , ** kwargs ): # pylint: disable=unused-argument
1029+ request = self .context .get ("request" )
1030+ comment = self .context .get ("comment" )
1031+ emoji = self .validated_data ["emoji" ]
1032+ reaction , _created = models .Reaction .objects .get_or_create (
1033+ comment = comment , emoji = emoji
1034+ )
1035+ if request and request .user :
1036+ reaction .add_user (request .user )
1037+ return reaction
0 commit comments