From 922735e5f2825275906275ea9ded09559fd1677a Mon Sep 17 00:00:00 2001 From: Nikhil <118773738+pablohashescobar@users.noreply.github.com> Date: Mon, 31 Jul 2023 10:42:17 +0530 Subject: [PATCH] feat: issue and comments reaction (#1674) * dev: initialize issue reactions * dev: issue reactions * dev: comment reactions and update in urls * dev: reactions in issue and comment list * dev: reaction filtering * dev: comment reaction lite serializer * fix: reaction delete endpoint query --- apiserver/plane/api/serializers/__init__.py | 2 + apiserver/plane/api/serializers/issue.py | 72 +++++++++++++ apiserver/plane/api/urls.py | 44 ++++++++ apiserver/plane/api/views/__init__.py | 2 + apiserver/plane/api/views/issue.py | 110 ++++++++++++++++++++ apiserver/plane/db/models/__init__.py | 2 + apiserver/plane/db/models/issue.py | 43 ++++++++ 7 files changed, 275 insertions(+) diff --git a/apiserver/plane/api/serializers/__init__.py b/apiserver/plane/api/serializers/__init__.py index 604175dba..2d38b1139 100644 --- a/apiserver/plane/api/serializers/__init__.py +++ b/apiserver/plane/api/serializers/__init__.py @@ -43,6 +43,8 @@ from .issue import ( IssueLiteSerializer, IssueAttachmentSerializer, IssueSubscriberSerializer, + IssueReactionSerializer, + CommentReactionSerializer, ) from .module import ( diff --git a/apiserver/plane/api/serializers/issue.py b/apiserver/plane/api/serializers/issue.py index 4af53b49a..ecbb1ca46 100644 --- a/apiserver/plane/api/serializers/issue.py +++ b/apiserver/plane/api/serializers/issue.py @@ -29,6 +29,8 @@ from plane.db.models import ( ModuleIssue, IssueLink, IssueAttachment, + IssueReaction, + CommentReaction, ) @@ -500,6 +502,74 @@ class IssueAttachmentSerializer(BaseSerializer): ] +class IssueReactionSerializer(BaseSerializer): + class Meta: + model = IssueReaction + fields = "__all__" + read_only_fields = [ + "workspace", + "project", + "issue", + "actor", + ] + + +class IssueReactionLiteSerializer(BaseSerializer): + actor_detail = UserLiteSerializer(read_only=True, source="actor") + + class Meta: + model = IssueReaction + fields = [ + "id", + "reaction", + "issue", + "actor_detail", + ] + + +class CommentReactionLiteSerializer(BaseSerializer): + actor_detail = UserLiteSerializer(read_only=True, source="actor") + + class Meta: + model = CommentReaction + fields = [ + "id", + "reaction", + "comment", + "actor_detail", + ] + + +class CommentReactionSerializer(BaseSerializer): + class Meta: + model = CommentReaction + fields = "__all__" + read_only_fields = ["workspace", "project", "comment", "actor"] + + + +class IssueCommentSerializer(BaseSerializer): + actor_detail = UserLiteSerializer(read_only=True, source="actor") + issue_detail = IssueFlatSerializer(read_only=True, source="issue") + project_detail = ProjectLiteSerializer(read_only=True, source="project") + workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace") + comment_reactions = CommentReactionLiteSerializer(read_only=True, many=True) + + + class Meta: + model = IssueComment + fields = "__all__" + read_only_fields = [ + "workspace", + "project", + "issue", + "created_by", + "updated_by", + "created_at", + "updated_at", + ] + + class IssueStateFlatSerializer(BaseSerializer): state_detail = StateLiteSerializer(read_only=True, source="state") project_detail = ProjectLiteSerializer(read_only=True, source="project") @@ -546,6 +616,7 @@ class IssueSerializer(BaseSerializer): issue_link = IssueLinkSerializer(read_only=True, many=True) issue_attachment = IssueAttachmentSerializer(read_only=True, many=True) sub_issues_count = serializers.IntegerField(read_only=True) + issue_reactions = IssueReactionLiteSerializer(read_only=True, many=True) class Meta: model = Issue @@ -571,6 +642,7 @@ class IssueLiteSerializer(BaseSerializer): module_id = serializers.UUIDField(read_only=True) attachment_count = serializers.IntegerField(read_only=True) link_count = serializers.IntegerField(read_only=True) + issue_reactions = IssueReactionLiteSerializer(read_only=True, many=True) class Meta: model = Issue diff --git a/apiserver/plane/api/urls.py b/apiserver/plane/api/urls.py index b9e1858e1..c8b5e7b5e 100644 --- a/apiserver/plane/api/urls.py +++ b/apiserver/plane/api/urls.py @@ -84,6 +84,8 @@ from plane.api.views import ( IssueAttachmentEndpoint, IssueArchiveViewSet, IssueSubscriberViewSet, + IssueReactionViewSet, + CommentReactionViewSet, ## End Issues # States StateViewSet, @@ -866,6 +868,48 @@ urlpatterns = [ name="project-issue-subscribers", ), ## End Issue Subscribers + # Issue Reactions + path( + "workspaces//projects//issues//reactions/", + IssueReactionViewSet.as_view( + { + "get": "list", + "post": "create", + } + ), + name="project-issue-reactions", + ), + path( + "workspaces//projects//issues//reactions//", + IssueReactionViewSet.as_view( + { + "delete": "destroy", + } + ), + name="project-issue-reactions", + ), + ## End Issue Reactions + # Comment Reactions + path( + "workspaces//projects//comments//reactions/", + CommentReactionViewSet.as_view( + { + "get": "list", + "post": "create", + } + ), + name="project-issue-comment-reactions", + ), + path( + "workspaces//projects//comments//reactions//", + CommentReactionViewSet.as_view( + { + "delete": "destroy", + } + ), + name="project-issue-comment-reactions", + ), + ## End Comment Reactions ## IssueProperty path( "workspaces//projects//issue-properties/", diff --git a/apiserver/plane/api/views/__init__.py b/apiserver/plane/api/views/__init__.py index 6c2fc0e5f..75509a16c 100644 --- a/apiserver/plane/api/views/__init__.py +++ b/apiserver/plane/api/views/__init__.py @@ -73,6 +73,8 @@ from .issue import ( IssueAttachmentEndpoint, IssueArchiveViewSet, IssueSubscriberViewSet, + CommentReactionViewSet, + IssueReactionViewSet, ) from .auth_extended import ( diff --git a/apiserver/plane/api/views/issue.py b/apiserver/plane/api/views/issue.py index 218c16d60..38d90ecf9 100644 --- a/apiserver/plane/api/views/issue.py +++ b/apiserver/plane/api/views/issue.py @@ -46,6 +46,8 @@ from plane.api.serializers import ( IssueAttachmentSerializer, IssueSubscriberSerializer, ProjectMemberLiteSerializer, + IssueReactionSerializer, + CommentReactionSerializer, ) from plane.api.permissions import ( WorkspaceEntityPermission, @@ -66,6 +68,8 @@ from plane.db.models import ( State, IssueSubscriber, ProjectMember, + IssueReaction, + CommentReaction, ) from plane.bgtasks.issue_activites_task import issue_activity from plane.utils.grouper import group_results @@ -152,6 +156,12 @@ class IssueViewSet(BaseViewSet): .select_related("parent") .prefetch_related("assignees") .prefetch_related("labels") + .prefetch_related( + Prefetch( + "issue_reactions", + queryset=IssueReaction.objects.select_related("actor"), + ) + ) ) @method_decorator(gzip_page) @@ -1335,3 +1345,103 @@ class IssueSubscriberViewSet(BaseViewSet): {"error": "Something went wrong, please try again later"}, status=status.HTTP_400_BAD_REQUEST, ) + + +class IssueReactionViewSet(BaseViewSet): + serializer_class = IssueReactionSerializer + model = IssueReaction + permission_classes = [ + ProjectLitePermission, + ] + + def get_queryset(self): + return ( + super() + .get_queryset() + .filter(workspace__slug=self.kwargs.get("slug")) + .filter(project_id=self.kwargs.get("project_id")) + .filter(issue_id=self.kwargs.get("issue_id")) + .filter(project__project_projectmember__member=self.request.user) + .order_by("-created_at") + .distinct() + ) + + def perform_create(self, serializer): + serializer.save( + issue_id=self.kwargs.get("issue_id"), + project_id=self.kwargs.get("project_id"), + actor=self.request.user, + ) + + def destroy(self, request, slug, project_id, issue_id, reaction_code): + try: + issue_reaction = IssueReaction.objects.get( + workspace__slug=slug, + project_id=project_id, + issue_id=issue_id, + reaction=reaction_code, + actor=request.user, + ) + issue_reaction.delete() + return Response(status=status.HTTP_204_NO_CONTENT) + except IssueReaction.DoesNotExist: + return Response( + {"error": "Issue reaction does not exist"}, + status=status.HTTP_400_BAD_REQUEST, + ) + except Exception as e: + capture_exception(e) + return Response( + {"error": "Something went wrong please try again later"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + +class CommentReactionViewSet(BaseViewSet): + serializer_class = CommentReactionSerializer + model = CommentReaction + permission_classes = [ + ProjectLitePermission, + ] + + def get_queryset(self): + return ( + super() + .get_queryset() + .filter(workspace__slug=self.kwargs.get("slug")) + .filter(project_id=self.kwargs.get("project_id")) + .filter(comment_id=self.kwargs.get("comment_id")) + .filter(project__project_projectmember__member=self.request.user) + .order_by("-created_at") + .distinct() + ) + + def perform_create(self, serializer): + serializer.save( + actor=self.request.user, + comment_id=self.kwargs.get("comment_id"), + project_id=self.kwargs.get("project_id"), + ) + + def destroy(self, request, slug, project_id, comment_id, reaction_code): + try: + comment_reaction = CommentReaction.objects.get( + workspace__slug=slug, + project_id=project_id, + comment_id=comment_id, + reaction=reaction_code, + actor=request.user, + ) + comment_reaction.delete() + return Response(status=status.HTTP_204_NO_CONTENT) + except CommentReaction.DoesNotExist: + return Response( + {"error": "Comment reaction does not exist"}, + status=status.HTTP_400_BAD_REQUEST, + ) + except Exception as e: + capture_exception(e) + return Response( + {"error": "Something went wrong please try again later"}, + status=status.HTTP_400_BAD_REQUEST, + ) diff --git a/apiserver/plane/db/models/__init__.py b/apiserver/plane/db/models/__init__.py index 1c075478d..959dea5f7 100644 --- a/apiserver/plane/db/models/__init__.py +++ b/apiserver/plane/db/models/__init__.py @@ -34,6 +34,8 @@ from .issue import ( IssueSequence, IssueAttachment, IssueSubscriber, + IssueReaction, + CommentReaction, ) from .asset import FileAsset diff --git a/apiserver/plane/db/models/issue.py b/apiserver/plane/db/models/issue.py index 78c2bbb40..2a4462942 100644 --- a/apiserver/plane/db/models/issue.py +++ b/apiserver/plane/db/models/issue.py @@ -424,6 +424,49 @@ class IssueSubscriber(ProjectBaseModel): return f"{self.issue.name} {self.subscriber.email}" +class IssueReaction(ProjectBaseModel): + + actor = models.ForeignKey( + settings.AUTH_USER_MODEL, + on_delete=models.CASCADE, + related_name="issue_reactions", + ) + issue = models.ForeignKey(Issue, on_delete=models.CASCADE, related_name="issue_reactions") + reaction = models.CharField(max_length=20) + + class Meta: + unique_together = ["issue", "actor", "reaction"] + verbose_name = "Issue Reaction" + verbose_name_plural = "Issue Reactions" + db_table = "issue_reactions" + ordering = ("-created_at",) + + def __str__(self): + return f"{self.issue.name} {self.actor.email}" + + +class CommentReaction(ProjectBaseModel): + + actor = models.ForeignKey( + settings.AUTH_USER_MODEL, + on_delete=models.CASCADE, + related_name="comment_reactions", + ) + comment = models.ForeignKey(IssueComment, on_delete=models.CASCADE, related_name="comment_reactions") + reaction = models.CharField(max_length=20) + + class Meta: + unique_together = ["comment", "actor", "reaction"] + verbose_name = "Comment Reaction" + verbose_name_plural = "Comment Reactions" + db_table = "comment_reactions" + ordering = ("-created_at",) + + def __str__(self): + return f"{self.issue.name} {self.actor.email}" + + + # TODO: Find a better method to save the model @receiver(post_save, sender=Issue) def create_issue_sequence(sender, instance, created, **kwargs):