From e275740a4597fd4d444debd318ae3f6ef6609672 Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Thu, 23 Nov 2023 17:58:45 +0530 Subject: [PATCH] dev: webhooks --- apiserver/plane/api/serializers/__init__.py | 4 +- apiserver/plane/api/serializers/cycle.py | 9 +++- apiserver/plane/api/serializers/issue.py | 31 ++++++++++--- apiserver/plane/api/serializers/module.py | 10 ++++- apiserver/plane/api/views/base.py | 2 +- apiserver/plane/api/views/cycle.py | 1 + apiserver/plane/app/views/base.py | 17 +++---- apiserver/plane/app/views/cycle.py | 3 ++ apiserver/plane/app/views/module.py | 2 + apiserver/plane/bgtasks/webhook_task.py | 49 ++++++++++++++------- apiserver/plane/db/models/module.py | 6 +-- 11 files changed, 97 insertions(+), 37 deletions(-) diff --git a/apiserver/plane/api/serializers/__init__.py b/apiserver/plane/api/serializers/__init__.py index 708e8b254..1fd1bce78 100644 --- a/apiserver/plane/api/serializers/__init__.py +++ b/apiserver/plane/api/serializers/__init__.py @@ -12,6 +12,6 @@ from .issue import ( IssueExpandSerializer, ) from .state import StateLiteSerializer, StateSerializer -from .cycle import CycleSerializer, CycleIssueSerializer -from .module import ModuleSerializer, ModuleIssueSerializer +from .cycle import CycleSerializer, CycleIssueSerializer, CycleLiteSerializer +from .module import ModuleSerializer, ModuleIssueSerializer, ModuleLiteSerializer from .inbox import InboxIssueSerializer \ No newline at end of file diff --git a/apiserver/plane/api/serializers/cycle.py b/apiserver/plane/api/serializers/cycle.py index b3e7708ef..5895a1bfc 100644 --- a/apiserver/plane/api/serializers/cycle.py +++ b/apiserver/plane/api/serializers/cycle.py @@ -46,4 +46,11 @@ class CycleIssueSerializer(BaseSerializer): "workspace", "project", "cycle", - ] \ No newline at end of file + ] + + +class CycleLiteSerializer(BaseSerializer): + + class Meta: + model = Cycle + fields = "__all__" \ No newline at end of file diff --git a/apiserver/plane/api/serializers/issue.py b/apiserver/plane/api/serializers/issue.py index 67968d559..2dbdddfc6 100644 --- a/apiserver/plane/api/serializers/issue.py +++ b/apiserver/plane/api/serializers/issue.py @@ -19,8 +19,9 @@ from plane.db.models import ( ProjectMember, ) from .base import BaseSerializer -from .cycle import CycleSerializer -from .module import ModuleSerializer +from .cycle import CycleSerializer, CycleLiteSerializer +from .module import ModuleSerializer, ModuleLiteSerializer + class IssueSerializer(BaseSerializer): assignees = serializers.ListField( @@ -312,10 +313,30 @@ class IssueActivitySerializer(BaseSerializer): ] -class IssueExpandSerializer(BaseSerializer): +class CycleIssueSerializer(BaseSerializer): + cycle = CycleSerializer(read_only=True) - cycle = CycleSerializer(source="issue_cycle.cycle", many=True) - module = ModuleSerializer(source="issue_module.module", many=True) + class Meta: + fields = [ + "cycle", + ] + + +class ModuleIssueSerializer(BaseSerializer): + module = ModuleSerializer(read_only=True) + + class Meta: + fields = [ + "module", + ] + + +class IssueExpandSerializer(BaseSerializer): + # Serialize the related cycle. It's a OneToOne relation. + cycle = CycleLiteSerializer(source="issue_cycle.cycle", read_only=True) + + # Serialize the related module. It's a OneToOne relation. + module = ModuleLiteSerializer(source="issue_module.module", read_only=True) class Meta: model = Issue diff --git a/apiserver/plane/api/serializers/module.py b/apiserver/plane/api/serializers/module.py index d91ac8a62..81d07f884 100644 --- a/apiserver/plane/api/serializers/module.py +++ b/apiserver/plane/api/serializers/module.py @@ -21,7 +21,6 @@ class ModuleSerializer(BaseSerializer): write_only=True, required=False, ) - is_favorite = serializers.BooleanField(read_only=True) total_issues = serializers.IntegerField(read_only=True) cancelled_issues = serializers.IntegerField(read_only=True) completed_issues = serializers.IntegerField(read_only=True) @@ -153,4 +152,11 @@ class ModuleLinkSerializer(BaseSerializer): raise serializers.ValidationError( {"error": "URL already exists for this Issue"} ) - return ModuleLink.objects.create(**validated_data) \ No newline at end of file + return ModuleLink.objects.create(**validated_data) + + +class ModuleLiteSerializer(BaseSerializer): + + class Meta: + model = Module + fields = "__all__" \ No newline at end of file diff --git a/apiserver/plane/api/views/base.py b/apiserver/plane/api/views/base.py index 9089f1cea..abde4e8b0 100644 --- a/apiserver/plane/api/views/base.py +++ b/apiserver/plane/api/views/base.py @@ -52,7 +52,7 @@ class WebhookMixin: # Push the object to delay send_webhook.delay( event=self.webhook_event, - event_data=response.data, + payload=response.data, kw=self.kwargs, action=self.request.method, slug=self.workspace_slug, diff --git a/apiserver/plane/api/views/cycle.py b/apiserver/plane/api/views/cycle.py index e20c845f2..310332333 100644 --- a/apiserver/plane/api/views/cycle.py +++ b/apiserver/plane/api/views/cycle.py @@ -311,6 +311,7 @@ class CycleIssueAPIEndpoint(WebhookMixin, BaseAPIView): serializer_class = CycleIssueSerializer model = CycleIssue webhook_event = "cycle_issue" + bulk = True permission_classes = [ ProjectEntityPermission, ] diff --git a/apiserver/plane/app/views/base.py b/apiserver/plane/app/views/base.py index ba18dbbfa..32449597b 100644 --- a/apiserver/plane/app/views/base.py +++ b/apiserver/plane/app/views/base.py @@ -43,27 +43,28 @@ class TimezoneMixin: 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"] + and self.request.method in ["POST", "PATCH", "DELETE"] and response.status_code in [200, 201, 204] ): - # Get the id - object_id = ( - response.data.get("id") if isinstance(response.data, dict) else None - ) - + # Push the object to delay send_webhook.delay( event=self.webhook_event, - event_id=object_id, + payload=response.data, + kw=self.kwargs, action=self.request.method, slug=self.workspace_slug, + bulk=self.bulk, ) - return response + return response class BaseViewSet(TimezoneMixin, ModelViewSet, BasePaginator): diff --git a/apiserver/plane/app/views/cycle.py b/apiserver/plane/app/views/cycle.py index dd1f3b4bb..b0fec588c 100644 --- a/apiserver/plane/app/views/cycle.py +++ b/apiserver/plane/app/views/cycle.py @@ -502,7 +502,10 @@ class CycleViewSet(WebhookMixin, BaseViewSet): class CycleIssueViewSet(WebhookMixin, BaseViewSet): serializer_class = CycleIssueSerializer model = CycleIssue + webhook_event = "cycle_issue" + bulk = True + permission_classes = [ ProjectEntityPermission, ] diff --git a/apiserver/plane/app/views/module.py b/apiserver/plane/app/views/module.py index d357f91a3..3a9968bfd 100644 --- a/apiserver/plane/app/views/module.py +++ b/apiserver/plane/app/views/module.py @@ -287,6 +287,8 @@ class ModuleIssueViewSet(WebhookMixin, BaseViewSet): serializer_class = ModuleIssueSerializer model = ModuleIssue webhook_event = "module_issue" + bulk = True + filterset_fields = [ "issue__labels__id", diff --git a/apiserver/plane/bgtasks/webhook_task.py b/apiserver/plane/bgtasks/webhook_task.py index 5665ebb45..144f2f267 100644 --- a/apiserver/plane/bgtasks/webhook_task.py +++ b/apiserver/plane/bgtasks/webhook_task.py @@ -55,11 +55,14 @@ MODEL_MAPPER = { } -def get_model_data(event, event_id, many=True): +def get_model_data(event, event_id, many=False): model = MODEL_MAPPER.get(event) - queryset = model.objects.get(pk=event_id) + if many: + queryset = model.objects.filter(pk__in=event_id) + else: + queryset = model.objects.get(pk=event_id) serializer = SERIALIZER_MAPPER.get(event) - return serializer(queryset).data + return serializer(queryset, many=many).data @shared_task( @@ -89,11 +92,11 @@ def webhook_task(self, webhook, slug, event, event_data, action): # Use HMAC for generating signature if webhook.secret_key: - event_data_json = json.dumps(event_data) if event_data is not None else '{}' + event_data_json = json.dumps(event_data) if event_data is not None else "{}" hmac_signature = hmac.new( webhook.secret_key.encode("utf-8"), event_data_json.encode("utf-8"), - hashlib.sha256 + hashlib.sha256, ) signature = hmac_signature.hexdigest() headers["X-Plane-Signature"] = signature @@ -157,7 +160,6 @@ def webhook_task(self, webhook, slug, event, event_data, action): raise requests.RequestException() except Exception as e: - print(e) if settings.DEBUG: print(e) capture_exception(e) @@ -165,7 +167,7 @@ def webhook_task(self, webhook, slug, event, event_data, action): @shared_task() -def send_webhook(event, event_data, kw, action, slug, bulk): +def send_webhook(event, payload, kw, action, slug, bulk): try: webhooks = Webhook.objects.filter(workspace__slug=slug, is_active=True) @@ -184,22 +186,39 @@ def send_webhook(event, event_data, kw, action, slug, bulk): if event == "issue_comment": webhooks = webhooks.filter(issue_comment=True) - if webhooks: if action in ["POST", "PATCH"]: - if bulk: - event_data= [] - if event in ["cycle_issue", "module_issue"]: - pass + if bulk and event in ["cycle_issue", "module_issue"]: + event_data = IssueExpandSerializer( + Issue.objects.filter( + pk__in=[ + str(event.get("issue")) for event in payload + ] + ).prefetch_related("issue_cycle", "issue_module"), many=True + ).data + event = "issue" + action = "PATCH" else: - event_id = event_data.get("id") if isinstance(event_data, dict) else None - event_data = [get_model_data(event=event, event_id=event_id, many=isinstance(event_id, list))] + event_data = [ + get_model_data( + event=event, + event_id=payload.get("id") if isinstance(payload, dict) else None, + 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,) + webhook_task.delay( + webhook=webhook.id, + slug=slug, + event=event, + event_data=data, + action=action, + ) except Exception as e: if settings.DEBUG: diff --git a/apiserver/plane/db/models/module.py b/apiserver/plane/db/models/module.py index e286d297a..ae540cc6c 100644 --- a/apiserver/plane/db/models/module.py +++ b/apiserver/plane/db/models/module.py @@ -51,9 +51,9 @@ class Module(ProjectBaseModel): def save(self, *args, **kwargs): if self._state.adding: - smallest_sort_order = Module.objects.filter( - project=self.project - ).aggregate(smallest=models.Min("sort_order"))["smallest"] + smallest_sort_order = Module.objects.filter(project=self.project).aggregate( + smallest=models.Min("sort_order") + )["smallest"] if smallest_sort_order is not None: self.sort_order = smallest_sort_order - 10000