forked from github/plane
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
This commit is contained in:
parent
ed75163ec4
commit
922735e5f2
@ -43,6 +43,8 @@ from .issue import (
|
|||||||
IssueLiteSerializer,
|
IssueLiteSerializer,
|
||||||
IssueAttachmentSerializer,
|
IssueAttachmentSerializer,
|
||||||
IssueSubscriberSerializer,
|
IssueSubscriberSerializer,
|
||||||
|
IssueReactionSerializer,
|
||||||
|
CommentReactionSerializer,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .module import (
|
from .module import (
|
||||||
|
@ -29,6 +29,8 @@ from plane.db.models import (
|
|||||||
ModuleIssue,
|
ModuleIssue,
|
||||||
IssueLink,
|
IssueLink,
|
||||||
IssueAttachment,
|
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):
|
class IssueStateFlatSerializer(BaseSerializer):
|
||||||
state_detail = StateLiteSerializer(read_only=True, source="state")
|
state_detail = StateLiteSerializer(read_only=True, source="state")
|
||||||
project_detail = ProjectLiteSerializer(read_only=True, source="project")
|
project_detail = ProjectLiteSerializer(read_only=True, source="project")
|
||||||
@ -546,6 +616,7 @@ class IssueSerializer(BaseSerializer):
|
|||||||
issue_link = IssueLinkSerializer(read_only=True, many=True)
|
issue_link = IssueLinkSerializer(read_only=True, many=True)
|
||||||
issue_attachment = IssueAttachmentSerializer(read_only=True, many=True)
|
issue_attachment = IssueAttachmentSerializer(read_only=True, many=True)
|
||||||
sub_issues_count = serializers.IntegerField(read_only=True)
|
sub_issues_count = serializers.IntegerField(read_only=True)
|
||||||
|
issue_reactions = IssueReactionLiteSerializer(read_only=True, many=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Issue
|
model = Issue
|
||||||
@ -571,6 +642,7 @@ class IssueLiteSerializer(BaseSerializer):
|
|||||||
module_id = serializers.UUIDField(read_only=True)
|
module_id = serializers.UUIDField(read_only=True)
|
||||||
attachment_count = serializers.IntegerField(read_only=True)
|
attachment_count = serializers.IntegerField(read_only=True)
|
||||||
link_count = serializers.IntegerField(read_only=True)
|
link_count = serializers.IntegerField(read_only=True)
|
||||||
|
issue_reactions = IssueReactionLiteSerializer(read_only=True, many=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Issue
|
model = Issue
|
||||||
|
@ -84,6 +84,8 @@ from plane.api.views import (
|
|||||||
IssueAttachmentEndpoint,
|
IssueAttachmentEndpoint,
|
||||||
IssueArchiveViewSet,
|
IssueArchiveViewSet,
|
||||||
IssueSubscriberViewSet,
|
IssueSubscriberViewSet,
|
||||||
|
IssueReactionViewSet,
|
||||||
|
CommentReactionViewSet,
|
||||||
## End Issues
|
## End Issues
|
||||||
# States
|
# States
|
||||||
StateViewSet,
|
StateViewSet,
|
||||||
@ -866,6 +868,48 @@ urlpatterns = [
|
|||||||
name="project-issue-subscribers",
|
name="project-issue-subscribers",
|
||||||
),
|
),
|
||||||
## End Issue Subscribers
|
## End Issue Subscribers
|
||||||
|
# Issue Reactions
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/reactions/",
|
||||||
|
IssueReactionViewSet.as_view(
|
||||||
|
{
|
||||||
|
"get": "list",
|
||||||
|
"post": "create",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="project-issue-reactions",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/reactions/<str:reaction_code>/",
|
||||||
|
IssueReactionViewSet.as_view(
|
||||||
|
{
|
||||||
|
"delete": "destroy",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="project-issue-reactions",
|
||||||
|
),
|
||||||
|
## End Issue Reactions
|
||||||
|
# Comment Reactions
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/comments/<uuid:comment_id>/reactions/",
|
||||||
|
CommentReactionViewSet.as_view(
|
||||||
|
{
|
||||||
|
"get": "list",
|
||||||
|
"post": "create",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="project-issue-comment-reactions",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/comments/<uuid:comment_id>/reactions/<str:reaction_code>/",
|
||||||
|
CommentReactionViewSet.as_view(
|
||||||
|
{
|
||||||
|
"delete": "destroy",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="project-issue-comment-reactions",
|
||||||
|
),
|
||||||
|
## End Comment Reactions
|
||||||
## IssueProperty
|
## IssueProperty
|
||||||
path(
|
path(
|
||||||
"workspaces/<str:slug>/projects/<uuid:project_id>/issue-properties/",
|
"workspaces/<str:slug>/projects/<uuid:project_id>/issue-properties/",
|
||||||
|
@ -73,6 +73,8 @@ from .issue import (
|
|||||||
IssueAttachmentEndpoint,
|
IssueAttachmentEndpoint,
|
||||||
IssueArchiveViewSet,
|
IssueArchiveViewSet,
|
||||||
IssueSubscriberViewSet,
|
IssueSubscriberViewSet,
|
||||||
|
CommentReactionViewSet,
|
||||||
|
IssueReactionViewSet,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .auth_extended import (
|
from .auth_extended import (
|
||||||
|
@ -46,6 +46,8 @@ from plane.api.serializers import (
|
|||||||
IssueAttachmentSerializer,
|
IssueAttachmentSerializer,
|
||||||
IssueSubscriberSerializer,
|
IssueSubscriberSerializer,
|
||||||
ProjectMemberLiteSerializer,
|
ProjectMemberLiteSerializer,
|
||||||
|
IssueReactionSerializer,
|
||||||
|
CommentReactionSerializer,
|
||||||
)
|
)
|
||||||
from plane.api.permissions import (
|
from plane.api.permissions import (
|
||||||
WorkspaceEntityPermission,
|
WorkspaceEntityPermission,
|
||||||
@ -66,6 +68,8 @@ from plane.db.models import (
|
|||||||
State,
|
State,
|
||||||
IssueSubscriber,
|
IssueSubscriber,
|
||||||
ProjectMember,
|
ProjectMember,
|
||||||
|
IssueReaction,
|
||||||
|
CommentReaction,
|
||||||
)
|
)
|
||||||
from plane.bgtasks.issue_activites_task import issue_activity
|
from plane.bgtasks.issue_activites_task import issue_activity
|
||||||
from plane.utils.grouper import group_results
|
from plane.utils.grouper import group_results
|
||||||
@ -152,6 +156,12 @@ class IssueViewSet(BaseViewSet):
|
|||||||
.select_related("parent")
|
.select_related("parent")
|
||||||
.prefetch_related("assignees")
|
.prefetch_related("assignees")
|
||||||
.prefetch_related("labels")
|
.prefetch_related("labels")
|
||||||
|
.prefetch_related(
|
||||||
|
Prefetch(
|
||||||
|
"issue_reactions",
|
||||||
|
queryset=IssueReaction.objects.select_related("actor"),
|
||||||
|
)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@method_decorator(gzip_page)
|
@method_decorator(gzip_page)
|
||||||
@ -1335,3 +1345,103 @@ class IssueSubscriberViewSet(BaseViewSet):
|
|||||||
{"error": "Something went wrong, please try again later"},
|
{"error": "Something went wrong, please try again later"},
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
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,
|
||||||
|
)
|
||||||
|
@ -34,6 +34,8 @@ from .issue import (
|
|||||||
IssueSequence,
|
IssueSequence,
|
||||||
IssueAttachment,
|
IssueAttachment,
|
||||||
IssueSubscriber,
|
IssueSubscriber,
|
||||||
|
IssueReaction,
|
||||||
|
CommentReaction,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .asset import FileAsset
|
from .asset import FileAsset
|
||||||
|
@ -424,6 +424,49 @@ class IssueSubscriber(ProjectBaseModel):
|
|||||||
return f"{self.issue.name} {self.subscriber.email}"
|
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
|
# TODO: Find a better method to save the model
|
||||||
@receiver(post_save, sender=Issue)
|
@receiver(post_save, sender=Issue)
|
||||||
def create_issue_sequence(sender, instance, created, **kwargs):
|
def create_issue_sequence(sender, instance, created, **kwargs):
|
||||||
|
Loading…
Reference in New Issue
Block a user