forked from github/plane
[WEB - 1122] fix: webhook for issues, issue comments, projects, cycles and modules. (#4330)
* dev: update webhook logic for issues * dev: update issue webhooks for cycle and module * dev: webhook for comment * dev: issue attachment webhooks * dev: add logging * dev: add inbox issue webhooks * dev: update the webhook send task * dev: project webhooks for api * dev: webhooks update for projects, cycles and modules * dev: fix webhook on cycle and module create from external apis
This commit is contained in:
parent
fb74875cde
commit
f1fda4ae4a
@ -1,9 +1,13 @@
|
|||||||
# Module improts
|
# Module improts
|
||||||
from .base import BaseSerializer
|
from .base import BaseSerializer
|
||||||
|
from .issue import IssueExpandSerializer
|
||||||
from plane.db.models import InboxIssue
|
from plane.db.models import InboxIssue
|
||||||
|
|
||||||
|
|
||||||
class InboxIssueSerializer(BaseSerializer):
|
class InboxIssueSerializer(BaseSerializer):
|
||||||
|
|
||||||
|
issue_detail = IssueExpandSerializer(read_only=True, source="issue")
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = InboxIssue
|
model = InboxIssue
|
||||||
fields = "__all__"
|
fields = "__all__"
|
||||||
|
@ -19,7 +19,6 @@ from rest_framework.views import APIView
|
|||||||
# Module imports
|
# Module imports
|
||||||
from plane.api.middleware.api_authentication import APIKeyAuthentication
|
from plane.api.middleware.api_authentication import APIKeyAuthentication
|
||||||
from plane.api.rate_limit import ApiKeyRateThrottle
|
from plane.api.rate_limit import ApiKeyRateThrottle
|
||||||
from plane.bgtasks.webhook_task import send_webhook
|
|
||||||
from plane.utils.exception_logger import log_exception
|
from plane.utils.exception_logger import log_exception
|
||||||
from plane.utils.paginator import BasePaginator
|
from plane.utils.paginator import BasePaginator
|
||||||
|
|
||||||
@ -38,40 +37,6 @@ class TimezoneMixin:
|
|||||||
timezone.deactivate()
|
timezone.deactivate()
|
||||||
|
|
||||||
|
|
||||||
class WebhookMixin:
|
|
||||||
webhook_event = None
|
|
||||||
bulk = False
|
|
||||||
|
|
||||||
def finalize_response(self, request, response, *args, **kwargs):
|
|
||||||
response = super().finalize_response(
|
|
||||||
request, response, *args, **kwargs
|
|
||||||
)
|
|
||||||
|
|
||||||
# Check for the case should webhook be sent
|
|
||||||
if (
|
|
||||||
self.webhook_event
|
|
||||||
and self.request.method in ["POST", "PATCH", "DELETE"]
|
|
||||||
and response.status_code in [200, 201, 204]
|
|
||||||
):
|
|
||||||
url = request.build_absolute_uri()
|
|
||||||
parsed_url = urlparse(url)
|
|
||||||
# Extract the scheme and netloc
|
|
||||||
scheme = parsed_url.scheme
|
|
||||||
netloc = parsed_url.netloc
|
|
||||||
# Push the object to delay
|
|
||||||
send_webhook.delay(
|
|
||||||
event=self.webhook_event,
|
|
||||||
payload=response.data,
|
|
||||||
kw=self.kwargs,
|
|
||||||
action=self.request.method,
|
|
||||||
slug=self.workspace_slug,
|
|
||||||
bulk=self.bulk,
|
|
||||||
current_site=f"{scheme}://{netloc}",
|
|
||||||
)
|
|
||||||
|
|
||||||
return response
|
|
||||||
|
|
||||||
|
|
||||||
class BaseAPIView(TimezoneMixin, APIView, BasePaginator):
|
class BaseAPIView(TimezoneMixin, APIView, BasePaginator):
|
||||||
authentication_classes = [
|
authentication_classes = [
|
||||||
APIKeyAuthentication,
|
APIKeyAuthentication,
|
||||||
|
@ -5,6 +5,7 @@ import json
|
|||||||
from django.core import serializers
|
from django.core import serializers
|
||||||
from django.db.models import Count, F, Func, OuterRef, Q, Sum
|
from django.db.models import Count, F, Func, OuterRef, Q, Sum
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
from django.core.serializers.json import DjangoJSONEncoder
|
||||||
|
|
||||||
# Third party imports
|
# Third party imports
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
@ -26,10 +27,11 @@ from plane.db.models import (
|
|||||||
)
|
)
|
||||||
from plane.utils.analytics_plot import burndown_plot
|
from plane.utils.analytics_plot import burndown_plot
|
||||||
|
|
||||||
from .base import BaseAPIView, WebhookMixin
|
from .base import BaseAPIView
|
||||||
|
from plane.bgtasks.webhook_task import model_activity
|
||||||
|
|
||||||
|
|
||||||
class CycleAPIEndpoint(WebhookMixin, BaseAPIView):
|
class CycleAPIEndpoint(BaseAPIView):
|
||||||
"""
|
"""
|
||||||
This viewset automatically provides `list`, `create`, `retrieve`,
|
This viewset automatically provides `list`, `create`, `retrieve`,
|
||||||
`update` and `destroy` actions related to cycle.
|
`update` and `destroy` actions related to cycle.
|
||||||
@ -277,6 +279,16 @@ class CycleAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
project_id=project_id,
|
project_id=project_id,
|
||||||
owned_by=request.user,
|
owned_by=request.user,
|
||||||
)
|
)
|
||||||
|
# Send the model activity
|
||||||
|
model_activity.delay(
|
||||||
|
model_name="cycle",
|
||||||
|
model_id=str(serializer.data["id"]),
|
||||||
|
requested_data=request.data,
|
||||||
|
current_instance=None,
|
||||||
|
actor_id=request.user.id,
|
||||||
|
slug=slug,
|
||||||
|
origin=request.META.get("HTTP_ORIGIN"),
|
||||||
|
)
|
||||||
return Response(
|
return Response(
|
||||||
serializer.data, status=status.HTTP_201_CREATED
|
serializer.data, status=status.HTTP_201_CREATED
|
||||||
)
|
)
|
||||||
@ -295,6 +307,11 @@ class CycleAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
cycle = Cycle.objects.get(
|
cycle = Cycle.objects.get(
|
||||||
workspace__slug=slug, project_id=project_id, pk=pk
|
workspace__slug=slug, project_id=project_id, pk=pk
|
||||||
)
|
)
|
||||||
|
|
||||||
|
current_instance = json.dumps(
|
||||||
|
CycleSerializer(cycle).data, cls=DjangoJSONEncoder
|
||||||
|
)
|
||||||
|
|
||||||
if cycle.archived_at:
|
if cycle.archived_at:
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "Archived cycle cannot be edited"},
|
{"error": "Archived cycle cannot be edited"},
|
||||||
@ -344,6 +361,17 @@ class CycleAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
status=status.HTTP_409_CONFLICT,
|
status=status.HTTP_409_CONFLICT,
|
||||||
)
|
)
|
||||||
serializer.save()
|
serializer.save()
|
||||||
|
|
||||||
|
# Send the model activity
|
||||||
|
model_activity.delay(
|
||||||
|
model_name="cycle",
|
||||||
|
model_id=str(serializer.data["id"]),
|
||||||
|
requested_data=request.data,
|
||||||
|
current_instance=current_instance,
|
||||||
|
actor_id=request.user.id,
|
||||||
|
slug=slug,
|
||||||
|
origin=request.META.get("HTTP_ORIGIN"),
|
||||||
|
)
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
@ -515,7 +543,7 @@ class CycleArchiveUnarchiveAPIEndpoint(BaseAPIView):
|
|||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
|
||||||
|
|
||||||
class CycleIssueAPIEndpoint(WebhookMixin, BaseAPIView):
|
class CycleIssueAPIEndpoint(BaseAPIView):
|
||||||
"""
|
"""
|
||||||
This viewset automatically provides `list`, `create`,
|
This viewset automatically provides `list`, `create`,
|
||||||
and `destroy` actions related to cycle issues.
|
and `destroy` actions related to cycle issues.
|
||||||
|
@ -154,6 +154,13 @@ class InboxIssueAPIEndpoint(BaseAPIView):
|
|||||||
state=state,
|
state=state,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# create an inbox issue
|
||||||
|
inbox_issue = InboxIssue.objects.create(
|
||||||
|
inbox_id=inbox.id,
|
||||||
|
project_id=project_id,
|
||||||
|
issue=issue,
|
||||||
|
source=request.data.get("source", "in-app"),
|
||||||
|
)
|
||||||
# Create an Issue Activity
|
# Create an Issue Activity
|
||||||
issue_activity.delay(
|
issue_activity.delay(
|
||||||
type="issue.activity.created",
|
type="issue.activity.created",
|
||||||
@ -163,14 +170,7 @@ class InboxIssueAPIEndpoint(BaseAPIView):
|
|||||||
project_id=str(project_id),
|
project_id=str(project_id),
|
||||||
current_instance=None,
|
current_instance=None,
|
||||||
epoch=int(timezone.now().timestamp()),
|
epoch=int(timezone.now().timestamp()),
|
||||||
)
|
inbox=str(inbox_issue.id),
|
||||||
|
|
||||||
# create an inbox issue
|
|
||||||
inbox_issue = InboxIssue.objects.create(
|
|
||||||
inbox_id=inbox.id,
|
|
||||||
project_id=project_id,
|
|
||||||
issue=issue,
|
|
||||||
source=request.data.get("source", "in-app"),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
serializer = InboxIssueSerializer(inbox_issue)
|
serializer = InboxIssueSerializer(inbox_issue)
|
||||||
@ -260,6 +260,7 @@ class InboxIssueAPIEndpoint(BaseAPIView):
|
|||||||
cls=DjangoJSONEncoder,
|
cls=DjangoJSONEncoder,
|
||||||
),
|
),
|
||||||
epoch=int(timezone.now().timestamp()),
|
epoch=int(timezone.now().timestamp()),
|
||||||
|
inbox=(inbox_issue.id),
|
||||||
)
|
)
|
||||||
issue_serializer.save()
|
issue_serializer.save()
|
||||||
else:
|
else:
|
||||||
@ -327,6 +328,7 @@ class InboxIssueAPIEndpoint(BaseAPIView):
|
|||||||
epoch=int(timezone.now().timestamp()),
|
epoch=int(timezone.now().timestamp()),
|
||||||
notification=False,
|
notification=False,
|
||||||
origin=request.META.get("HTTP_ORIGIN"),
|
origin=request.META.get("HTTP_ORIGIN"),
|
||||||
|
inbox=str(inbox_issue.id),
|
||||||
)
|
)
|
||||||
|
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
|
@ -48,11 +48,10 @@ from plane.db.models import (
|
|||||||
ProjectMember,
|
ProjectMember,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .base import BaseAPIView, WebhookMixin
|
from .base import BaseAPIView
|
||||||
|
|
||||||
|
|
||||||
|
class WorkspaceIssueAPIEndpoint(BaseAPIView):
|
||||||
class WorkspaceIssueAPIEndpoint(WebhookMixin, BaseAPIView):
|
|
||||||
"""
|
"""
|
||||||
This viewset provides `retrieveByIssueId` on workspace level
|
This viewset provides `retrieveByIssueId` on workspace level
|
||||||
|
|
||||||
@ -60,12 +59,9 @@ class WorkspaceIssueAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
|
|
||||||
model = Issue
|
model = Issue
|
||||||
webhook_event = "issue"
|
webhook_event = "issue"
|
||||||
permission_classes = [
|
permission_classes = [ProjectEntityPermission]
|
||||||
ProjectEntityPermission
|
|
||||||
]
|
|
||||||
serializer_class = IssueSerializer
|
serializer_class = IssueSerializer
|
||||||
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def project__identifier(self):
|
def project__identifier(self):
|
||||||
return self.kwargs.get("project__identifier", None)
|
return self.kwargs.get("project__identifier", None)
|
||||||
@ -91,7 +87,9 @@ class WorkspaceIssueAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
.order_by(self.kwargs.get("order_by", "-created_at"))
|
.order_by(self.kwargs.get("order_by", "-created_at"))
|
||||||
).distinct()
|
).distinct()
|
||||||
|
|
||||||
def get(self, request, slug, project__identifier=None, issue__identifier=None):
|
def get(
|
||||||
|
self, request, slug, project__identifier=None, issue__identifier=None
|
||||||
|
):
|
||||||
if issue__identifier and project__identifier:
|
if issue__identifier and project__identifier:
|
||||||
issue = Issue.issue_objects.annotate(
|
issue = Issue.issue_objects.annotate(
|
||||||
sub_issues_count=Issue.issue_objects.filter(
|
sub_issues_count=Issue.issue_objects.filter(
|
||||||
@ -100,7 +98,11 @@ class WorkspaceIssueAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
.order_by()
|
.order_by()
|
||||||
.annotate(count=Func(F("id"), function="Count"))
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
.values("count")
|
.values("count")
|
||||||
).get(workspace__slug=slug, project__identifier=project__identifier, sequence_id=issue__identifier)
|
).get(
|
||||||
|
workspace__slug=slug,
|
||||||
|
project__identifier=project__identifier,
|
||||||
|
sequence_id=issue__identifier,
|
||||||
|
)
|
||||||
return Response(
|
return Response(
|
||||||
IssueSerializer(
|
IssueSerializer(
|
||||||
issue,
|
issue,
|
||||||
@ -110,7 +112,8 @@ class WorkspaceIssueAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
status=status.HTTP_200_OK,
|
status=status.HTTP_200_OK,
|
||||||
)
|
)
|
||||||
|
|
||||||
class IssueAPIEndpoint(WebhookMixin, BaseAPIView):
|
|
||||||
|
class IssueAPIEndpoint(BaseAPIView):
|
||||||
"""
|
"""
|
||||||
This viewset automatically provides `list`, `create`, `retrieve`,
|
This viewset automatically provides `list`, `create`, `retrieve`,
|
||||||
`update` and `destroy` actions related to issue.
|
`update` and `destroy` actions related to issue.
|
||||||
@ -652,7 +655,7 @@ class IssueLinkAPIEndpoint(BaseAPIView):
|
|||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
|
||||||
|
|
||||||
class IssueCommentAPIEndpoint(WebhookMixin, BaseAPIView):
|
class IssueCommentAPIEndpoint(BaseAPIView):
|
||||||
"""
|
"""
|
||||||
This viewset automatically provides `list`, `create`, `retrieve`,
|
This viewset automatically provides `list`, `create`, `retrieve`,
|
||||||
`update` and `destroy` actions related to comments of the particular issue.
|
`update` and `destroy` actions related to comments of the particular issue.
|
||||||
|
@ -5,6 +5,7 @@ import json
|
|||||||
from django.core import serializers
|
from django.core import serializers
|
||||||
from django.db.models import Count, F, Func, OuterRef, Prefetch, Q
|
from django.db.models import Count, F, Func, OuterRef, Prefetch, Q
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
from django.core.serializers.json import DjangoJSONEncoder
|
||||||
|
|
||||||
# Third party imports
|
# Third party imports
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
@ -28,10 +29,11 @@ from plane.db.models import (
|
|||||||
Project,
|
Project,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .base import BaseAPIView, WebhookMixin
|
from .base import BaseAPIView
|
||||||
|
from plane.bgtasks.webhook_task import model_activity
|
||||||
|
|
||||||
|
|
||||||
class ModuleAPIEndpoint(WebhookMixin, BaseAPIView):
|
class ModuleAPIEndpoint(BaseAPIView):
|
||||||
"""
|
"""
|
||||||
This viewset automatically provides `list`, `create`, `retrieve`,
|
This viewset automatically provides `list`, `create`, `retrieve`,
|
||||||
`update` and `destroy` actions related to module.
|
`update` and `destroy` actions related to module.
|
||||||
@ -163,6 +165,16 @@ class ModuleAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
status=status.HTTP_409_CONFLICT,
|
status=status.HTTP_409_CONFLICT,
|
||||||
)
|
)
|
||||||
serializer.save()
|
serializer.save()
|
||||||
|
# Send the model activity
|
||||||
|
model_activity.delay(
|
||||||
|
model_name="module",
|
||||||
|
model_id=str(serializer.data["id"]),
|
||||||
|
requested_data=request.data,
|
||||||
|
current_instance=None,
|
||||||
|
actor_id=request.user.id,
|
||||||
|
slug=slug,
|
||||||
|
origin=request.META.get("HTTP_ORIGIN"),
|
||||||
|
)
|
||||||
module = Module.objects.get(pk=serializer.data["id"])
|
module = Module.objects.get(pk=serializer.data["id"])
|
||||||
serializer = ModuleSerializer(module)
|
serializer = ModuleSerializer(module)
|
||||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||||
@ -172,6 +184,11 @@ class ModuleAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
module = Module.objects.get(
|
module = Module.objects.get(
|
||||||
pk=pk, project_id=project_id, workspace__slug=slug
|
pk=pk, project_id=project_id, workspace__slug=slug
|
||||||
)
|
)
|
||||||
|
|
||||||
|
current_instance = json.dumps(
|
||||||
|
ModuleSerializer(module).data, cls=DjangoJSONEncoder
|
||||||
|
)
|
||||||
|
|
||||||
if module.archived_at:
|
if module.archived_at:
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "Archived module cannot be edited"},
|
{"error": "Archived module cannot be edited"},
|
||||||
@ -204,6 +221,18 @@ class ModuleAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
status=status.HTTP_409_CONFLICT,
|
status=status.HTTP_409_CONFLICT,
|
||||||
)
|
)
|
||||||
serializer.save()
|
serializer.save()
|
||||||
|
|
||||||
|
# Send the model activity
|
||||||
|
model_activity.delay(
|
||||||
|
model_name="module",
|
||||||
|
model_id=str(serializer.data["id"]),
|
||||||
|
requested_data=request.data,
|
||||||
|
current_instance=current_instance,
|
||||||
|
actor_id=request.user.id,
|
||||||
|
slug=slug,
|
||||||
|
origin=request.META.get("HTTP_ORIGIN"),
|
||||||
|
)
|
||||||
|
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
@ -260,7 +289,7 @@ class ModuleAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
|
||||||
|
|
||||||
class ModuleIssueAPIEndpoint(WebhookMixin, BaseAPIView):
|
class ModuleIssueAPIEndpoint(BaseAPIView):
|
||||||
"""
|
"""
|
||||||
This viewset automatically provides `list`, `create`, `retrieve`,
|
This viewset automatically provides `list`, `create`, `retrieve`,
|
||||||
`update` and `destroy` actions related to module issues.
|
`update` and `destroy` actions related to module issues.
|
||||||
|
@ -1,7 +1,11 @@
|
|||||||
|
# Python imports
|
||||||
|
import json
|
||||||
|
|
||||||
# Django imports
|
# Django imports
|
||||||
from django.db import IntegrityError
|
from django.db import IntegrityError
|
||||||
from django.db.models import Exists, F, Func, OuterRef, Prefetch, Q, Subquery
|
from django.db.models import Exists, F, Func, OuterRef, Prefetch, Q, Subquery
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
from django.core.serializers.json import DjangoJSONEncoder
|
||||||
|
|
||||||
# Third party imports
|
# Third party imports
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
@ -23,11 +27,11 @@ from plane.db.models import (
|
|||||||
State,
|
State,
|
||||||
Workspace,
|
Workspace,
|
||||||
)
|
)
|
||||||
|
from plane.bgtasks.webhook_task import model_activity
|
||||||
from .base import BaseAPIView, WebhookMixin
|
from .base import BaseAPIView
|
||||||
|
|
||||||
|
|
||||||
class ProjectAPIEndpoint(WebhookMixin, BaseAPIView):
|
class ProjectAPIEndpoint(BaseAPIView):
|
||||||
"""Project Endpoints to create, update, list, retrieve and delete endpoint"""
|
"""Project Endpoints to create, update, list, retrieve and delete endpoint"""
|
||||||
|
|
||||||
serializer_class = ProjectSerializer
|
serializer_class = ProjectSerializer
|
||||||
@ -236,6 +240,17 @@ class ProjectAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
.filter(pk=serializer.data["id"])
|
.filter(pk=serializer.data["id"])
|
||||||
.first()
|
.first()
|
||||||
)
|
)
|
||||||
|
# Model activity
|
||||||
|
model_activity.delay(
|
||||||
|
model_name="project",
|
||||||
|
model_id=str(project.id),
|
||||||
|
requested_data=request.data,
|
||||||
|
current_instance=None,
|
||||||
|
actor_id=request.user.id,
|
||||||
|
slug=slug,
|
||||||
|
origin=request.META.get("HTTP_ORIGIN"),
|
||||||
|
)
|
||||||
|
|
||||||
serializer = ProjectSerializer(project)
|
serializer = ProjectSerializer(project)
|
||||||
return Response(
|
return Response(
|
||||||
serializer.data, status=status.HTTP_201_CREATED
|
serializer.data, status=status.HTTP_201_CREATED
|
||||||
@ -265,7 +280,9 @@ class ProjectAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
try:
|
try:
|
||||||
workspace = Workspace.objects.get(slug=slug)
|
workspace = Workspace.objects.get(slug=slug)
|
||||||
project = Project.objects.get(pk=pk)
|
project = Project.objects.get(pk=pk)
|
||||||
|
current_instance = json.dumps(
|
||||||
|
ProjectSerializer(project).data, cls=DjangoJSONEncoder
|
||||||
|
)
|
||||||
if project.archived_at:
|
if project.archived_at:
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "Archived project cannot be updated"},
|
{"error": "Archived project cannot be updated"},
|
||||||
@ -303,6 +320,17 @@ class ProjectAPIEndpoint(WebhookMixin, BaseAPIView):
|
|||||||
.filter(pk=serializer.data["id"])
|
.filter(pk=serializer.data["id"])
|
||||||
.first()
|
.first()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
model_activity.delay(
|
||||||
|
model_name="project",
|
||||||
|
model_id=str(project.id),
|
||||||
|
requested_data=request.data,
|
||||||
|
current_instance=current_instance,
|
||||||
|
actor_id=request.user.id,
|
||||||
|
slug=slug,
|
||||||
|
origin=request.META.get("HTTP_ORIGIN"),
|
||||||
|
)
|
||||||
|
|
||||||
serializer = ProjectSerializer(project)
|
serializer = ProjectSerializer(project)
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
return Response(
|
return Response(
|
||||||
|
@ -442,7 +442,7 @@ class IssueLinkSerializer(BaseSerializer):
|
|||||||
raise serializers.ValidationError("Invalid URL format.")
|
raise serializers.ValidationError("Invalid URL format.")
|
||||||
|
|
||||||
# Check URL scheme
|
# Check URL scheme
|
||||||
if not value.startswith(('http://', 'https://')):
|
if not value.startswith(("http://", "https://")):
|
||||||
raise serializers.ValidationError("Invalid URL scheme.")
|
raise serializers.ValidationError("Invalid URL scheme.")
|
||||||
|
|
||||||
return value
|
return value
|
||||||
|
@ -30,7 +30,7 @@ from .user.base import (
|
|||||||
|
|
||||||
from .oauth import OauthEndpoint
|
from .oauth import OauthEndpoint
|
||||||
|
|
||||||
from .base import BaseAPIView, BaseViewSet, WebhookMixin
|
from .base import BaseAPIView, BaseViewSet
|
||||||
|
|
||||||
from .workspace.base import (
|
from .workspace.base import (
|
||||||
WorkSpaceViewSet,
|
WorkSpaceViewSet,
|
||||||
|
@ -19,7 +19,6 @@ from rest_framework.views import APIView
|
|||||||
from rest_framework.viewsets import ModelViewSet
|
from rest_framework.viewsets import ModelViewSet
|
||||||
|
|
||||||
# Module imports
|
# Module imports
|
||||||
from plane.bgtasks.webhook_task import send_webhook
|
|
||||||
from plane.utils.exception_logger import log_exception
|
from plane.utils.exception_logger import log_exception
|
||||||
from plane.utils.paginator import BasePaginator
|
from plane.utils.paginator import BasePaginator
|
||||||
|
|
||||||
@ -38,35 +37,6 @@ class TimezoneMixin:
|
|||||||
timezone.deactivate()
|
timezone.deactivate()
|
||||||
|
|
||||||
|
|
||||||
class WebhookMixin:
|
|
||||||
webhook_event = None
|
|
||||||
bulk = False
|
|
||||||
|
|
||||||
def finalize_response(self, request, response, *args, **kwargs):
|
|
||||||
response = super().finalize_response(
|
|
||||||
request, response, *args, **kwargs
|
|
||||||
)
|
|
||||||
|
|
||||||
# Check for the case should webhook be sent
|
|
||||||
if (
|
|
||||||
self.webhook_event
|
|
||||||
and self.request.method in ["POST", "PATCH", "DELETE"]
|
|
||||||
and response.status_code in [200, 201, 204]
|
|
||||||
):
|
|
||||||
# Push the object to delay
|
|
||||||
send_webhook.delay(
|
|
||||||
event=self.webhook_event,
|
|
||||||
payload=response.data,
|
|
||||||
kw=self.kwargs,
|
|
||||||
action=self.request.method,
|
|
||||||
slug=self.workspace_slug,
|
|
||||||
bulk=self.bulk,
|
|
||||||
current_site=request.META.get("HTTP_ORIGIN"),
|
|
||||||
)
|
|
||||||
|
|
||||||
return response
|
|
||||||
|
|
||||||
|
|
||||||
class BaseViewSet(TimezoneMixin, ModelViewSet, BasePaginator):
|
class BaseViewSet(TimezoneMixin, ModelViewSet, BasePaginator):
|
||||||
model = None
|
model = None
|
||||||
|
|
||||||
|
@ -20,6 +20,7 @@ from django.db.models import (
|
|||||||
)
|
)
|
||||||
from django.db.models.functions import Coalesce
|
from django.db.models.functions import Coalesce
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
from django.core.serializers.json import DjangoJSONEncoder
|
||||||
|
|
||||||
# Third party imports
|
# Third party imports
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
@ -47,10 +48,11 @@ from plane.db.models import (
|
|||||||
from plane.utils.analytics_plot import burndown_plot
|
from plane.utils.analytics_plot import burndown_plot
|
||||||
|
|
||||||
# Module imports
|
# Module imports
|
||||||
from .. import BaseAPIView, BaseViewSet, WebhookMixin
|
from .. import BaseAPIView, BaseViewSet
|
||||||
|
from plane.bgtasks.webhook_task import model_activity
|
||||||
|
|
||||||
|
|
||||||
class CycleViewSet(WebhookMixin, BaseViewSet):
|
class CycleViewSet(BaseViewSet):
|
||||||
serializer_class = CycleSerializer
|
serializer_class = CycleSerializer
|
||||||
model = Cycle
|
model = Cycle
|
||||||
webhook_event = "cycle"
|
webhook_event = "cycle"
|
||||||
@ -412,6 +414,17 @@ class CycleViewSet(WebhookMixin, BaseViewSet):
|
|||||||
)
|
)
|
||||||
.first()
|
.first()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Send the model activity
|
||||||
|
model_activity.delay(
|
||||||
|
model_name="cycle",
|
||||||
|
model_id=str(cycle["id"]),
|
||||||
|
requested_data=request.data,
|
||||||
|
current_instance=None,
|
||||||
|
actor_id=request.user.id,
|
||||||
|
slug=slug,
|
||||||
|
origin=request.META.get("HTTP_ORIGIN"),
|
||||||
|
)
|
||||||
return Response(cycle, status=status.HTTP_201_CREATED)
|
return Response(cycle, status=status.HTTP_201_CREATED)
|
||||||
return Response(
|
return Response(
|
||||||
serializer.errors, status=status.HTTP_400_BAD_REQUEST
|
serializer.errors, status=status.HTTP_400_BAD_REQUEST
|
||||||
@ -434,6 +447,11 @@ class CycleViewSet(WebhookMixin, BaseViewSet):
|
|||||||
{"error": "Archived cycle cannot be updated"},
|
{"error": "Archived cycle cannot be updated"},
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
current_instance = json.dumps(
|
||||||
|
CycleSerializer(cycle).data, cls=DjangoJSONEncoder
|
||||||
|
)
|
||||||
|
|
||||||
request_data = request.data
|
request_data = request.data
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@ -487,6 +505,18 @@ class CycleViewSet(WebhookMixin, BaseViewSet):
|
|||||||
"assignee_ids",
|
"assignee_ids",
|
||||||
"status",
|
"status",
|
||||||
).first()
|
).first()
|
||||||
|
|
||||||
|
# Send the model activity
|
||||||
|
model_activity.delay(
|
||||||
|
model_name="cycle",
|
||||||
|
model_id=str(cycle["id"]),
|
||||||
|
requested_data=request.data,
|
||||||
|
current_instance=current_instance,
|
||||||
|
actor_id=request.user.id,
|
||||||
|
slug=slug,
|
||||||
|
origin=request.META.get("HTTP_ORIGIN"),
|
||||||
|
)
|
||||||
|
|
||||||
return Response(cycle, status=status.HTTP_200_OK)
|
return Response(cycle, status=status.HTTP_200_OK)
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
@ -23,7 +23,7 @@ from rest_framework.response import Response
|
|||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
|
|
||||||
# Module imports
|
# Module imports
|
||||||
from .. import BaseViewSet, WebhookMixin
|
from .. import BaseViewSet
|
||||||
from plane.app.serializers import (
|
from plane.app.serializers import (
|
||||||
IssueSerializer,
|
IssueSerializer,
|
||||||
CycleIssueSerializer,
|
CycleIssueSerializer,
|
||||||
@ -40,7 +40,7 @@ from plane.bgtasks.issue_activites_task import issue_activity
|
|||||||
from plane.utils.issue_filters import issue_filters
|
from plane.utils.issue_filters import issue_filters
|
||||||
from plane.utils.user_timezone_converter import user_timezone_converter
|
from plane.utils.user_timezone_converter import user_timezone_converter
|
||||||
|
|
||||||
class CycleIssueViewSet(WebhookMixin, BaseViewSet):
|
class CycleIssueViewSet(BaseViewSet):
|
||||||
serializer_class = CycleIssueSerializer
|
serializer_class = CycleIssueSerializer
|
||||||
model = CycleIssue
|
model = CycleIssue
|
||||||
|
|
||||||
@ -254,6 +254,7 @@ class CycleIssueViewSet(WebhookMixin, BaseViewSet):
|
|||||||
update_cycle_issue_activity = []
|
update_cycle_issue_activity = []
|
||||||
# Iterate over each cycle_issue in cycle_issues
|
# Iterate over each cycle_issue in cycle_issues
|
||||||
for cycle_issue in cycle_issues:
|
for cycle_issue in cycle_issues:
|
||||||
|
old_cycle_id = cycle_issue.cycle_id
|
||||||
# Update the cycle_issue's cycle_id
|
# Update the cycle_issue's cycle_id
|
||||||
cycle_issue.cycle_id = cycle_id
|
cycle_issue.cycle_id = cycle_id
|
||||||
# Add the modified cycle_issue to the records_to_update list
|
# Add the modified cycle_issue to the records_to_update list
|
||||||
@ -261,7 +262,7 @@ class CycleIssueViewSet(WebhookMixin, BaseViewSet):
|
|||||||
# Record the update activity
|
# Record the update activity
|
||||||
update_cycle_issue_activity.append(
|
update_cycle_issue_activity.append(
|
||||||
{
|
{
|
||||||
"old_cycle_id": str(cycle_issue.cycle_id),
|
"old_cycle_id": str(old_cycle_id),
|
||||||
"new_cycle_id": str(cycle_id),
|
"new_cycle_id": str(cycle_id),
|
||||||
"issue_id": str(cycle_issue.issue_id),
|
"issue_id": str(cycle_issue.issue_id),
|
||||||
}
|
}
|
||||||
|
@ -251,6 +251,16 @@ class InboxIssueViewSet(BaseViewSet):
|
|||||||
)
|
)
|
||||||
if serializer.is_valid():
|
if serializer.is_valid():
|
||||||
serializer.save()
|
serializer.save()
|
||||||
|
inbox_id = Inbox.objects.filter(
|
||||||
|
workspace__slug=slug, project_id=project_id
|
||||||
|
).first()
|
||||||
|
# create an inbox issue
|
||||||
|
inbox_issue = InboxIssue.objects.create(
|
||||||
|
inbox_id=inbox_id.id,
|
||||||
|
project_id=project_id,
|
||||||
|
issue_id=serializer.data["id"],
|
||||||
|
source=request.data.get("source", "in-app"),
|
||||||
|
)
|
||||||
# Create an Issue Activity
|
# Create an Issue Activity
|
||||||
issue_activity.delay(
|
issue_activity.delay(
|
||||||
type="issue.activity.created",
|
type="issue.activity.created",
|
||||||
@ -262,16 +272,7 @@ class InboxIssueViewSet(BaseViewSet):
|
|||||||
epoch=int(timezone.now().timestamp()),
|
epoch=int(timezone.now().timestamp()),
|
||||||
notification=True,
|
notification=True,
|
||||||
origin=request.META.get("HTTP_ORIGIN"),
|
origin=request.META.get("HTTP_ORIGIN"),
|
||||||
)
|
inbox=str(inbox_issue.id),
|
||||||
inbox_id = Inbox.objects.filter(
|
|
||||||
workspace__slug=slug, project_id=project_id
|
|
||||||
).first()
|
|
||||||
# create an inbox issue
|
|
||||||
inbox_issue = InboxIssue.objects.create(
|
|
||||||
inbox_id=inbox_id.id,
|
|
||||||
project_id=project_id,
|
|
||||||
issue_id=serializer.data["id"],
|
|
||||||
source=request.data.get("source", "in-app"),
|
|
||||||
)
|
)
|
||||||
inbox_issue = (
|
inbox_issue = (
|
||||||
InboxIssue.objects.select_related("issue")
|
InboxIssue.objects.select_related("issue")
|
||||||
@ -339,7 +340,24 @@ class InboxIssueViewSet(BaseViewSet):
|
|||||||
# Get issue data
|
# Get issue data
|
||||||
issue_data = request.data.pop("issue", False)
|
issue_data = request.data.pop("issue", False)
|
||||||
if bool(issue_data):
|
if bool(issue_data):
|
||||||
issue = Issue.objects.get(
|
issue = Issue.objects.annotate(
|
||||||
|
label_ids=Coalesce(
|
||||||
|
ArrayAgg(
|
||||||
|
"labels__id",
|
||||||
|
distinct=True,
|
||||||
|
filter=~Q(labels__id__isnull=True),
|
||||||
|
),
|
||||||
|
Value([], output_field=ArrayField(UUIDField())),
|
||||||
|
),
|
||||||
|
assignee_ids=Coalesce(
|
||||||
|
ArrayAgg(
|
||||||
|
"assignees__id",
|
||||||
|
distinct=True,
|
||||||
|
filter=~Q(assignees__id__isnull=True),
|
||||||
|
),
|
||||||
|
Value([], output_field=ArrayField(UUIDField())),
|
||||||
|
),
|
||||||
|
).get(
|
||||||
pk=inbox_issue.issue_id,
|
pk=inbox_issue.issue_id,
|
||||||
workspace__slug=slug,
|
workspace__slug=slug,
|
||||||
project_id=project_id,
|
project_id=project_id,
|
||||||
@ -379,6 +397,7 @@ class InboxIssueViewSet(BaseViewSet):
|
|||||||
epoch=int(timezone.now().timestamp()),
|
epoch=int(timezone.now().timestamp()),
|
||||||
notification=True,
|
notification=True,
|
||||||
origin=request.META.get("HTTP_ORIGIN"),
|
origin=request.META.get("HTTP_ORIGIN"),
|
||||||
|
inbox=str(inbox_issue.id),
|
||||||
)
|
)
|
||||||
issue_serializer.save()
|
issue_serializer.save()
|
||||||
else:
|
else:
|
||||||
@ -444,6 +463,7 @@ class InboxIssueViewSet(BaseViewSet):
|
|||||||
epoch=int(timezone.now().timestamp()),
|
epoch=int(timezone.now().timestamp()),
|
||||||
notification=False,
|
notification=False,
|
||||||
origin=request.META.get("HTTP_ORIGIN"),
|
origin=request.META.get("HTTP_ORIGIN"),
|
||||||
|
inbox=(inbox_issue.id),
|
||||||
)
|
)
|
||||||
|
|
||||||
inbox_issue = (
|
inbox_issue = (
|
||||||
@ -480,7 +500,8 @@ class InboxIssueViewSet(BaseViewSet):
|
|||||||
output_field=ArrayField(UUIDField()),
|
output_field=ArrayField(UUIDField()),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
).first()
|
)
|
||||||
|
.first()
|
||||||
)
|
)
|
||||||
serializer = InboxIssueDetailSerializer(inbox_issue).data
|
serializer = InboxIssueDetailSerializer(inbox_issue).data
|
||||||
return Response(serializer, status=status.HTTP_200_OK)
|
return Response(serializer, status=status.HTTP_200_OK)
|
||||||
|
@ -53,7 +53,7 @@ from plane.utils.issue_filters import issue_filters
|
|||||||
from plane.utils.user_timezone_converter import user_timezone_converter
|
from plane.utils.user_timezone_converter import user_timezone_converter
|
||||||
|
|
||||||
# Module imports
|
# Module imports
|
||||||
from .. import BaseAPIView, BaseViewSet, WebhookMixin
|
from .. import BaseAPIView, BaseViewSet
|
||||||
|
|
||||||
|
|
||||||
class IssueListEndpoint(BaseAPIView):
|
class IssueListEndpoint(BaseAPIView):
|
||||||
@ -249,7 +249,7 @@ class IssueListEndpoint(BaseAPIView):
|
|||||||
return Response(issues, status=status.HTTP_200_OK)
|
return Response(issues, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
class IssueViewSet(WebhookMixin, BaseViewSet):
|
class IssueViewSet(BaseViewSet):
|
||||||
def get_serializer_class(self):
|
def get_serializer_class(self):
|
||||||
return (
|
return (
|
||||||
IssueCreateSerializer
|
IssueCreateSerializer
|
||||||
|
@ -11,7 +11,7 @@ from rest_framework.response import Response
|
|||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
|
|
||||||
# Module imports
|
# Module imports
|
||||||
from .. import BaseViewSet, WebhookMixin
|
from .. import BaseViewSet
|
||||||
from plane.app.serializers import (
|
from plane.app.serializers import (
|
||||||
IssueCommentSerializer,
|
IssueCommentSerializer,
|
||||||
CommentReactionSerializer,
|
CommentReactionSerializer,
|
||||||
@ -25,7 +25,7 @@ from plane.db.models import (
|
|||||||
from plane.bgtasks.issue_activites_task import issue_activity
|
from plane.bgtasks.issue_activites_task import issue_activity
|
||||||
|
|
||||||
|
|
||||||
class IssueCommentViewSet(WebhookMixin, BaseViewSet):
|
class IssueCommentViewSet(BaseViewSet):
|
||||||
serializer_class = IssueCommentSerializer
|
serializer_class = IssueCommentSerializer
|
||||||
model = IssueComment
|
model = IssueComment
|
||||||
webhook_event = "issue_comment"
|
webhook_event = "issue_comment"
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
# Python imports
|
# Python imports
|
||||||
import json
|
import json
|
||||||
|
|
||||||
|
# Django Imports
|
||||||
from django.contrib.postgres.aggregates import ArrayAgg
|
from django.contrib.postgres.aggregates import ArrayAgg
|
||||||
from django.contrib.postgres.fields import ArrayField
|
from django.contrib.postgres.fields import ArrayField
|
||||||
from django.db.models import (
|
from django.db.models import (
|
||||||
@ -17,14 +18,14 @@ from django.db.models import (
|
|||||||
Value,
|
Value,
|
||||||
)
|
)
|
||||||
from django.db.models.functions import Coalesce
|
from django.db.models.functions import Coalesce
|
||||||
|
from django.core.serializers.json import DjangoJSONEncoder
|
||||||
# Django Imports
|
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from rest_framework import status
|
|
||||||
|
|
||||||
# Third party imports
|
# Third party imports
|
||||||
|
from rest_framework import status
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
|
||||||
|
# Module imports
|
||||||
from plane.app.permissions import (
|
from plane.app.permissions import (
|
||||||
ProjectEntityPermission,
|
ProjectEntityPermission,
|
||||||
ProjectLitePermission,
|
ProjectLitePermission,
|
||||||
@ -49,13 +50,11 @@ from plane.db.models import (
|
|||||||
)
|
)
|
||||||
from plane.utils.analytics_plot import burndown_plot
|
from plane.utils.analytics_plot import burndown_plot
|
||||||
from plane.utils.user_timezone_converter import user_timezone_converter
|
from plane.utils.user_timezone_converter import user_timezone_converter
|
||||||
|
from plane.bgtasks.webhook_task import model_activity
|
||||||
|
from .. import BaseAPIView, BaseViewSet
|
||||||
|
|
||||||
|
|
||||||
# Module imports
|
class ModuleViewSet(BaseViewSet):
|
||||||
from .. import BaseAPIView, BaseViewSet, WebhookMixin
|
|
||||||
|
|
||||||
|
|
||||||
class ModuleViewSet(WebhookMixin, BaseViewSet):
|
|
||||||
model = Module
|
model = Module
|
||||||
permission_classes = [
|
permission_classes = [
|
||||||
ProjectEntityPermission,
|
ProjectEntityPermission,
|
||||||
@ -238,6 +237,16 @@ class ModuleViewSet(WebhookMixin, BaseViewSet):
|
|||||||
"updated_at",
|
"updated_at",
|
||||||
)
|
)
|
||||||
).first()
|
).first()
|
||||||
|
# Send the model activity
|
||||||
|
model_activity.delay(
|
||||||
|
model_name="module",
|
||||||
|
model_id=str(module["id"]),
|
||||||
|
requested_data=request.data,
|
||||||
|
current_instance=None,
|
||||||
|
actor_id=request.user.id,
|
||||||
|
slug=slug,
|
||||||
|
origin=request.META.get("HTTP_ORIGIN"),
|
||||||
|
)
|
||||||
datetime_fields = ["created_at", "updated_at"]
|
datetime_fields = ["created_at", "updated_at"]
|
||||||
module = user_timezone_converter(
|
module = user_timezone_converter(
|
||||||
module, datetime_fields, request.user.user_timezone
|
module, datetime_fields, request.user.user_timezone
|
||||||
@ -422,6 +431,9 @@ class ModuleViewSet(WebhookMixin, BaseViewSet):
|
|||||||
|
|
||||||
def partial_update(self, request, slug, project_id, pk):
|
def partial_update(self, request, slug, project_id, pk):
|
||||||
module = self.get_queryset().filter(pk=pk)
|
module = self.get_queryset().filter(pk=pk)
|
||||||
|
current_instance = json.dumps(
|
||||||
|
ModuleSerializer(module).data, cls=DjangoJSONEncoder
|
||||||
|
)
|
||||||
|
|
||||||
if module.first().archived_at:
|
if module.first().archived_at:
|
||||||
return Response(
|
return Response(
|
||||||
@ -464,6 +476,18 @@ class ModuleViewSet(WebhookMixin, BaseViewSet):
|
|||||||
"created_at",
|
"created_at",
|
||||||
"updated_at",
|
"updated_at",
|
||||||
).first()
|
).first()
|
||||||
|
|
||||||
|
# Send the model activity
|
||||||
|
model_activity.delay(
|
||||||
|
model_name="module",
|
||||||
|
model_id=str(module["id"]),
|
||||||
|
requested_data=request.data,
|
||||||
|
current_instance=current_instance,
|
||||||
|
actor_id=request.user.id,
|
||||||
|
slug=slug,
|
||||||
|
origin=request.META.get("HTTP_ORIGIN"),
|
||||||
|
)
|
||||||
|
|
||||||
datetime_fields = ["created_at", "updated_at"]
|
datetime_fields = ["created_at", "updated_at"]
|
||||||
module = user_timezone_converter(
|
module = user_timezone_converter(
|
||||||
module, datetime_fields, request.user.user_timezone
|
module, datetime_fields, request.user.user_timezone
|
||||||
|
@ -16,7 +16,7 @@ from rest_framework.response import Response
|
|||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
|
|
||||||
# Module imports
|
# Module imports
|
||||||
from .. import BaseViewSet, WebhookMixin
|
from .. import BaseViewSet
|
||||||
from plane.app.serializers import (
|
from plane.app.serializers import (
|
||||||
ModuleIssueSerializer,
|
ModuleIssueSerializer,
|
||||||
IssueSerializer,
|
IssueSerializer,
|
||||||
@ -33,7 +33,7 @@ from plane.bgtasks.issue_activites_task import issue_activity
|
|||||||
from plane.utils.issue_filters import issue_filters
|
from plane.utils.issue_filters import issue_filters
|
||||||
from plane.utils.user_timezone_converter import user_timezone_converter
|
from plane.utils.user_timezone_converter import user_timezone_converter
|
||||||
|
|
||||||
class ModuleIssueViewSet(WebhookMixin, BaseViewSet):
|
class ModuleIssueViewSet(BaseViewSet):
|
||||||
serializer_class = ModuleIssueSerializer
|
serializer_class = ModuleIssueSerializer
|
||||||
model = ModuleIssue
|
model = ModuleIssue
|
||||||
webhook_event = "module_issue"
|
webhook_event = "module_issue"
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
# Python imports
|
# Python imports
|
||||||
import boto3
|
import boto3
|
||||||
|
import json
|
||||||
|
|
||||||
# Django imports
|
# Django imports
|
||||||
from django.db import IntegrityError
|
from django.db import IntegrityError
|
||||||
@ -14,6 +15,7 @@ from django.db.models import (
|
|||||||
)
|
)
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
from django.core.serializers.json import DjangoJSONEncoder
|
||||||
|
|
||||||
# Third Party imports
|
# Third Party imports
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
@ -22,7 +24,7 @@ from rest_framework import serializers
|
|||||||
from rest_framework.permissions import AllowAny
|
from rest_framework.permissions import AllowAny
|
||||||
|
|
||||||
# Module imports
|
# Module imports
|
||||||
from plane.app.views.base import BaseViewSet, BaseAPIView, WebhookMixin
|
from plane.app.views.base import BaseViewSet, BaseAPIView
|
||||||
from plane.app.serializers import (
|
from plane.app.serializers import (
|
||||||
ProjectSerializer,
|
ProjectSerializer,
|
||||||
ProjectListSerializer,
|
ProjectListSerializer,
|
||||||
@ -50,9 +52,10 @@ from plane.db.models import (
|
|||||||
Issue,
|
Issue,
|
||||||
)
|
)
|
||||||
from plane.utils.cache import cache_response
|
from plane.utils.cache import cache_response
|
||||||
|
from plane.bgtasks.webhook_task import model_activity
|
||||||
|
|
||||||
|
|
||||||
class ProjectViewSet(WebhookMixin, BaseViewSet):
|
class ProjectViewSet(BaseViewSet):
|
||||||
serializer_class = ProjectListSerializer
|
serializer_class = ProjectListSerializer
|
||||||
model = Project
|
model = Project
|
||||||
webhook_event = "project"
|
webhook_event = "project"
|
||||||
@ -334,6 +337,17 @@ class ProjectViewSet(WebhookMixin, BaseViewSet):
|
|||||||
.filter(pk=serializer.data["id"])
|
.filter(pk=serializer.data["id"])
|
||||||
.first()
|
.first()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
model_activity.delay(
|
||||||
|
model_name="project",
|
||||||
|
model_id=str(project.id),
|
||||||
|
requested_data=request.data,
|
||||||
|
current_instance=None,
|
||||||
|
actor_id=request.user.id,
|
||||||
|
slug=slug,
|
||||||
|
origin=request.META.get("HTTP_ORIGIN"),
|
||||||
|
)
|
||||||
|
|
||||||
serializer = ProjectListSerializer(project)
|
serializer = ProjectListSerializer(project)
|
||||||
return Response(
|
return Response(
|
||||||
serializer.data, status=status.HTTP_201_CREATED
|
serializer.data, status=status.HTTP_201_CREATED
|
||||||
@ -364,7 +378,9 @@ class ProjectViewSet(WebhookMixin, BaseViewSet):
|
|||||||
workspace = Workspace.objects.get(slug=slug)
|
workspace = Workspace.objects.get(slug=slug)
|
||||||
|
|
||||||
project = Project.objects.get(pk=pk)
|
project = Project.objects.get(pk=pk)
|
||||||
|
current_instance = json.dumps(
|
||||||
|
ProjectSerializer(project).data, cls=DjangoJSONEncoder
|
||||||
|
)
|
||||||
if project.archived_at:
|
if project.archived_at:
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "Archived projects cannot be updated"},
|
{"error": "Archived projects cannot be updated"},
|
||||||
@ -402,6 +418,16 @@ class ProjectViewSet(WebhookMixin, BaseViewSet):
|
|||||||
.filter(pk=serializer.data["id"])
|
.filter(pk=serializer.data["id"])
|
||||||
.first()
|
.first()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
model_activity.delay(
|
||||||
|
model_name="project",
|
||||||
|
model_id=str(project.id),
|
||||||
|
requested_data=request.data,
|
||||||
|
current_instance=current_instance,
|
||||||
|
actor_id=request.user.id,
|
||||||
|
slug=slug,
|
||||||
|
origin=request.META.get("HTTP_ORIGIN"),
|
||||||
|
)
|
||||||
serializer = ProjectListSerializer(project)
|
serializer = ProjectListSerializer(project)
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
return Response(
|
return Response(
|
||||||
|
@ -31,6 +31,7 @@ from plane.db.models import (
|
|||||||
)
|
)
|
||||||
from plane.settings.redis import redis_instance
|
from plane.settings.redis import redis_instance
|
||||||
from plane.utils.exception_logger import log_exception
|
from plane.utils.exception_logger import log_exception
|
||||||
|
from plane.bgtasks.webhook_task import webhook_activity
|
||||||
|
|
||||||
|
|
||||||
# Track Changes in name
|
# Track Changes in name
|
||||||
@ -1296,7 +1297,7 @@ def create_issue_vote_activity(
|
|||||||
IssueActivity(
|
IssueActivity(
|
||||||
issue_id=issue_id,
|
issue_id=issue_id,
|
||||||
actor_id=actor_id,
|
actor_id=actor_id,
|
||||||
verb="created",
|
verb="updated",
|
||||||
old_value=None,
|
old_value=None,
|
||||||
new_value=requested_data.get("vote"),
|
new_value=requested_data.get("vote"),
|
||||||
field="vote",
|
field="vote",
|
||||||
@ -1365,7 +1366,7 @@ def create_issue_relation_activity(
|
|||||||
IssueActivity(
|
IssueActivity(
|
||||||
issue_id=issue_id,
|
issue_id=issue_id,
|
||||||
actor_id=actor_id,
|
actor_id=actor_id,
|
||||||
verb="created",
|
verb="updated",
|
||||||
old_value="",
|
old_value="",
|
||||||
new_value=f"{issue.project.identifier}-{issue.sequence_id}",
|
new_value=f"{issue.project.identifier}-{issue.sequence_id}",
|
||||||
field=requested_data.get("relation_type"),
|
field=requested_data.get("relation_type"),
|
||||||
@ -1380,7 +1381,7 @@ def create_issue_relation_activity(
|
|||||||
IssueActivity(
|
IssueActivity(
|
||||||
issue_id=related_issue,
|
issue_id=related_issue,
|
||||||
actor_id=actor_id,
|
actor_id=actor_id,
|
||||||
verb="created",
|
verb="updated",
|
||||||
old_value="",
|
old_value="",
|
||||||
new_value=f"{issue.project.identifier}-{issue.sequence_id}",
|
new_value=f"{issue.project.identifier}-{issue.sequence_id}",
|
||||||
field=(
|
field=(
|
||||||
@ -1606,6 +1607,7 @@ def issue_activity(
|
|||||||
subscriber=True,
|
subscriber=True,
|
||||||
notification=False,
|
notification=False,
|
||||||
origin=None,
|
origin=None,
|
||||||
|
inbox=None,
|
||||||
):
|
):
|
||||||
try:
|
try:
|
||||||
issue_activities = []
|
issue_activities = []
|
||||||
@ -1692,6 +1694,41 @@ def issue_activity(
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
log_exception(e)
|
log_exception(e)
|
||||||
|
|
||||||
|
for activity in issue_activities_created:
|
||||||
|
webhook_activity.delay(
|
||||||
|
event=(
|
||||||
|
"issue_comment"
|
||||||
|
if activity.field == "comment"
|
||||||
|
else "inbox_issue" if inbox else "issue"
|
||||||
|
),
|
||||||
|
event_id=(
|
||||||
|
activity.issue_comment_id
|
||||||
|
if activity.field == "comment"
|
||||||
|
else inbox if inbox else activity.issue_id
|
||||||
|
),
|
||||||
|
verb=activity.verb,
|
||||||
|
field=(
|
||||||
|
"description"
|
||||||
|
if activity.field == "comment"
|
||||||
|
else activity.field
|
||||||
|
),
|
||||||
|
old_value=(
|
||||||
|
activity.old_value
|
||||||
|
if activity.old_value != ""
|
||||||
|
else None
|
||||||
|
),
|
||||||
|
new_value=(
|
||||||
|
activity.new_value
|
||||||
|
if activity.new_value != ""
|
||||||
|
else None
|
||||||
|
),
|
||||||
|
actor_id=activity.actor_id,
|
||||||
|
current_site=origin,
|
||||||
|
slug=activity.workspace.slug,
|
||||||
|
old_identifier=activity.old_identifier,
|
||||||
|
new_identifier=activity.new_identifier,
|
||||||
|
)
|
||||||
|
|
||||||
if notification:
|
if notification:
|
||||||
notifications.delay(
|
notifications.delay(
|
||||||
type=type,
|
type=type,
|
||||||
|
@ -25,6 +25,8 @@ from plane.api.serializers import (
|
|||||||
ModuleIssueSerializer,
|
ModuleIssueSerializer,
|
||||||
ModuleSerializer,
|
ModuleSerializer,
|
||||||
ProjectSerializer,
|
ProjectSerializer,
|
||||||
|
UserLiteSerializer,
|
||||||
|
InboxIssueSerializer,
|
||||||
)
|
)
|
||||||
from plane.db.models import (
|
from plane.db.models import (
|
||||||
Cycle,
|
Cycle,
|
||||||
@ -37,6 +39,7 @@ from plane.db.models import (
|
|||||||
User,
|
User,
|
||||||
Webhook,
|
Webhook,
|
||||||
WebhookLog,
|
WebhookLog,
|
||||||
|
InboxIssue,
|
||||||
)
|
)
|
||||||
from plane.license.utils.instance_value import get_email_configuration
|
from plane.license.utils.instance_value import get_email_configuration
|
||||||
from plane.utils.exception_logger import log_exception
|
from plane.utils.exception_logger import log_exception
|
||||||
@ -49,6 +52,8 @@ SERIALIZER_MAPPER = {
|
|||||||
"cycle_issue": CycleIssueSerializer,
|
"cycle_issue": CycleIssueSerializer,
|
||||||
"module_issue": ModuleIssueSerializer,
|
"module_issue": ModuleIssueSerializer,
|
||||||
"issue_comment": IssueCommentSerializer,
|
"issue_comment": IssueCommentSerializer,
|
||||||
|
"user": UserLiteSerializer,
|
||||||
|
"inbox_issue": InboxIssueSerializer,
|
||||||
}
|
}
|
||||||
|
|
||||||
MODEL_MAPPER = {
|
MODEL_MAPPER = {
|
||||||
@ -59,6 +64,8 @@ MODEL_MAPPER = {
|
|||||||
"cycle_issue": CycleIssue,
|
"cycle_issue": CycleIssue,
|
||||||
"module_issue": ModuleIssue,
|
"module_issue": ModuleIssue,
|
||||||
"issue_comment": IssueComment,
|
"issue_comment": IssueComment,
|
||||||
|
"user": User,
|
||||||
|
"inbox_issue": InboxIssue,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -179,64 +186,6 @@ def webhook_task(self, webhook, slug, event, event_data, action, current_site):
|
|||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
@shared_task()
|
|
||||||
def send_webhook(event, payload, kw, action, slug, bulk, current_site):
|
|
||||||
try:
|
|
||||||
webhooks = Webhook.objects.filter(workspace__slug=slug, is_active=True)
|
|
||||||
|
|
||||||
if event == "project":
|
|
||||||
webhooks = webhooks.filter(project=True)
|
|
||||||
|
|
||||||
if event == "issue":
|
|
||||||
webhooks = webhooks.filter(issue=True)
|
|
||||||
|
|
||||||
if event == "module" or event == "module_issue":
|
|
||||||
webhooks = webhooks.filter(module=True)
|
|
||||||
|
|
||||||
if event == "cycle" or event == "cycle_issue":
|
|
||||||
webhooks = webhooks.filter(cycle=True)
|
|
||||||
|
|
||||||
if event == "issue_comment":
|
|
||||||
webhooks = webhooks.filter(issue_comment=True)
|
|
||||||
|
|
||||||
if webhooks:
|
|
||||||
if action in ["POST", "PATCH"]:
|
|
||||||
if bulk and event in ["cycle_issue", "module_issue"]:
|
|
||||||
return
|
|
||||||
else:
|
|
||||||
event_data = [
|
|
||||||
get_model_data(
|
|
||||||
event=event,
|
|
||||||
event_id=(
|
|
||||||
payload.get("id")
|
|
||||||
if isinstance(payload, dict)
|
|
||||||
else kw.get("pk")
|
|
||||||
),
|
|
||||||
many=False,
|
|
||||||
)
|
|
||||||
]
|
|
||||||
|
|
||||||
if action == "DELETE":
|
|
||||||
event_data = [{"id": kw.get("pk")}]
|
|
||||||
|
|
||||||
for webhook in webhooks:
|
|
||||||
for data in event_data:
|
|
||||||
webhook_task.delay(
|
|
||||||
webhook=webhook.id,
|
|
||||||
slug=slug,
|
|
||||||
event=event,
|
|
||||||
event_data=data,
|
|
||||||
action=action,
|
|
||||||
current_site=current_site,
|
|
||||||
)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
if settings.DEBUG:
|
|
||||||
print(e)
|
|
||||||
log_exception(e)
|
|
||||||
return
|
|
||||||
|
|
||||||
|
|
||||||
@shared_task
|
@shared_task
|
||||||
def send_webhook_deactivation_email(
|
def send_webhook_deactivation_email(
|
||||||
webhook_id, receiver_id, current_site, reason
|
webhook_id, receiver_id, current_site, reason
|
||||||
@ -294,3 +243,240 @@ def send_webhook_deactivation_email(
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
log_exception(e)
|
log_exception(e)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
|
@shared_task(
|
||||||
|
bind=True,
|
||||||
|
autoretry_for=(requests.RequestException,),
|
||||||
|
retry_backoff=600,
|
||||||
|
max_retries=5,
|
||||||
|
retry_jitter=True,
|
||||||
|
)
|
||||||
|
def webhook_send_task(
|
||||||
|
self,
|
||||||
|
webhook,
|
||||||
|
slug,
|
||||||
|
event,
|
||||||
|
event_data,
|
||||||
|
action,
|
||||||
|
current_site,
|
||||||
|
activity,
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
webhook = Webhook.objects.get(id=webhook, workspace__slug=slug)
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"User-Agent": "Autopilot",
|
||||||
|
"X-Plane-Delivery": str(uuid.uuid4()),
|
||||||
|
"X-Plane-Event": event,
|
||||||
|
}
|
||||||
|
|
||||||
|
# # Your secret key
|
||||||
|
event_data = (
|
||||||
|
json.loads(json.dumps(event_data, cls=DjangoJSONEncoder))
|
||||||
|
if event_data is not None
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
|
||||||
|
activity = (
|
||||||
|
json.loads(json.dumps(activity, cls=DjangoJSONEncoder))
|
||||||
|
if activity is not None
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
|
||||||
|
action = {
|
||||||
|
"POST": "create",
|
||||||
|
"PATCH": "update",
|
||||||
|
"PUT": "update",
|
||||||
|
"DELETE": "delete",
|
||||||
|
}.get(action, action)
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
"event": event,
|
||||||
|
"action": action,
|
||||||
|
"webhook_id": str(webhook.id),
|
||||||
|
"workspace_id": str(webhook.workspace_id),
|
||||||
|
"data": event_data,
|
||||||
|
"activity": activity,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Use HMAC for generating signature
|
||||||
|
if webhook.secret_key:
|
||||||
|
hmac_signature = hmac.new(
|
||||||
|
webhook.secret_key.encode("utf-8"),
|
||||||
|
json.dumps(payload).encode("utf-8"),
|
||||||
|
hashlib.sha256,
|
||||||
|
)
|
||||||
|
signature = hmac_signature.hexdigest()
|
||||||
|
headers["X-Plane-Signature"] = signature
|
||||||
|
|
||||||
|
# Send the webhook event
|
||||||
|
response = requests.post(
|
||||||
|
webhook.url,
|
||||||
|
headers=headers,
|
||||||
|
json=payload,
|
||||||
|
timeout=30,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Log the webhook request
|
||||||
|
WebhookLog.objects.create(
|
||||||
|
workspace_id=str(webhook.workspace_id),
|
||||||
|
webhook_id=str(webhook.id),
|
||||||
|
event_type=str(event),
|
||||||
|
request_method=str(action),
|
||||||
|
request_headers=str(headers),
|
||||||
|
request_body=str(payload),
|
||||||
|
response_status=str(response.status_code),
|
||||||
|
response_headers=str(response.headers),
|
||||||
|
response_body=str(response.text),
|
||||||
|
retry_count=str(self.request.retries),
|
||||||
|
)
|
||||||
|
|
||||||
|
except requests.RequestException as e:
|
||||||
|
# Log the failed webhook request
|
||||||
|
WebhookLog.objects.create(
|
||||||
|
workspace_id=str(webhook.workspace_id),
|
||||||
|
webhook_id=str(webhook.id),
|
||||||
|
event_type=str(event),
|
||||||
|
request_method=str(action),
|
||||||
|
request_headers=str(headers),
|
||||||
|
request_body=str(payload),
|
||||||
|
response_status=500,
|
||||||
|
response_headers="",
|
||||||
|
response_body=str(e),
|
||||||
|
retry_count=str(self.request.retries),
|
||||||
|
)
|
||||||
|
# Retry logic
|
||||||
|
if self.request.retries >= self.max_retries:
|
||||||
|
Webhook.objects.filter(pk=webhook.id).update(is_active=False)
|
||||||
|
if webhook:
|
||||||
|
# send email for the deactivation of the webhook
|
||||||
|
send_webhook_deactivation_email(
|
||||||
|
webhook_id=webhook.id,
|
||||||
|
receiver_id=webhook.created_by_id,
|
||||||
|
reason=str(e),
|
||||||
|
current_site=current_site,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
raise requests.RequestException()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
if settings.DEBUG:
|
||||||
|
print(e)
|
||||||
|
log_exception(e)
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
@shared_task
|
||||||
|
def webhook_activity(
|
||||||
|
event,
|
||||||
|
verb,
|
||||||
|
field,
|
||||||
|
old_value,
|
||||||
|
new_value,
|
||||||
|
actor_id,
|
||||||
|
slug,
|
||||||
|
current_site,
|
||||||
|
event_id,
|
||||||
|
old_identifier,
|
||||||
|
new_identifier,
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
webhooks = Webhook.objects.filter(workspace__slug=slug, is_active=True)
|
||||||
|
|
||||||
|
if event == "project":
|
||||||
|
webhooks = webhooks.filter(project=True)
|
||||||
|
|
||||||
|
if event == "issue":
|
||||||
|
webhooks = webhooks.filter(issue=True)
|
||||||
|
|
||||||
|
if event == "module" or event == "module_issue":
|
||||||
|
webhooks = webhooks.filter(module=True)
|
||||||
|
|
||||||
|
if event == "cycle" or event == "cycle_issue":
|
||||||
|
webhooks = webhooks.filter(cycle=True)
|
||||||
|
|
||||||
|
if event == "issue_comment":
|
||||||
|
webhooks = webhooks.filter(issue_comment=True)
|
||||||
|
|
||||||
|
for webhook in webhooks:
|
||||||
|
webhook_send_task.delay(
|
||||||
|
webhook=webhook.id,
|
||||||
|
slug=slug,
|
||||||
|
event=event,
|
||||||
|
event_data=get_model_data(
|
||||||
|
event=event,
|
||||||
|
event_id=event_id,
|
||||||
|
),
|
||||||
|
action=verb,
|
||||||
|
current_site=current_site,
|
||||||
|
activity={
|
||||||
|
"field": field,
|
||||||
|
"new_value": new_value,
|
||||||
|
"old_value": old_value,
|
||||||
|
"actor": get_model_data(event="user", event_id=actor_id),
|
||||||
|
"old_identifier": old_identifier,
|
||||||
|
"new_identifier": new_identifier,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return
|
||||||
|
except Exception as e:
|
||||||
|
if settings.DEBUG:
|
||||||
|
print(e)
|
||||||
|
log_exception(e)
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
@shared_task
|
||||||
|
def model_activity(
|
||||||
|
model_name,
|
||||||
|
model_id,
|
||||||
|
requested_data,
|
||||||
|
current_instance,
|
||||||
|
actor_id,
|
||||||
|
slug,
|
||||||
|
origin=None,
|
||||||
|
):
|
||||||
|
"""Function takes in two json and computes differences between keys of both the json"""
|
||||||
|
if current_instance is None:
|
||||||
|
webhook_activity.delay(
|
||||||
|
event=model_name,
|
||||||
|
verb="created",
|
||||||
|
field=None,
|
||||||
|
old_value=None,
|
||||||
|
new_value=None,
|
||||||
|
actor_id=actor_id,
|
||||||
|
slug=slug,
|
||||||
|
current_site=origin,
|
||||||
|
event_id=model_id,
|
||||||
|
old_identifier=None,
|
||||||
|
new_identifier=None,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Load the current instance
|
||||||
|
current_instance = (
|
||||||
|
json.loads(current_instance) if current_instance is not None else None
|
||||||
|
)
|
||||||
|
|
||||||
|
# Loop through all keys in requested data and check the current value and requested value
|
||||||
|
for key in requested_data:
|
||||||
|
current_value = current_instance.get(key, None)
|
||||||
|
requested_value = requested_data.get(key, None)
|
||||||
|
if current_value != requested_value:
|
||||||
|
webhook_activity.delay(
|
||||||
|
event=model_name,
|
||||||
|
verb="updated",
|
||||||
|
field=key,
|
||||||
|
old_value=current_value,
|
||||||
|
new_value=requested_value,
|
||||||
|
actor_id=actor_id,
|
||||||
|
slug=slug,
|
||||||
|
current_site=origin,
|
||||||
|
event_id=model_id,
|
||||||
|
old_identifier=None,
|
||||||
|
new_identifier=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
return
|
||||||
|
Loading…
Reference in New Issue
Block a user