dev: webhooks

This commit is contained in:
pablohashescobar 2023-11-23 17:58:45 +05:30
parent 2e513af4f3
commit e275740a45
11 changed files with 97 additions and 37 deletions

View File

@ -12,6 +12,6 @@ from .issue import (
IssueExpandSerializer, IssueExpandSerializer,
) )
from .state import StateLiteSerializer, StateSerializer from .state import StateLiteSerializer, StateSerializer
from .cycle import CycleSerializer, CycleIssueSerializer from .cycle import CycleSerializer, CycleIssueSerializer, CycleLiteSerializer
from .module import ModuleSerializer, ModuleIssueSerializer from .module import ModuleSerializer, ModuleIssueSerializer, ModuleLiteSerializer
from .inbox import InboxIssueSerializer from .inbox import InboxIssueSerializer

View File

@ -47,3 +47,10 @@ class CycleIssueSerializer(BaseSerializer):
"project", "project",
"cycle", "cycle",
] ]
class CycleLiteSerializer(BaseSerializer):
class Meta:
model = Cycle
fields = "__all__"

View File

@ -19,8 +19,9 @@ from plane.db.models import (
ProjectMember, ProjectMember,
) )
from .base import BaseSerializer from .base import BaseSerializer
from .cycle import CycleSerializer from .cycle import CycleSerializer, CycleLiteSerializer
from .module import ModuleSerializer from .module import ModuleSerializer, ModuleLiteSerializer
class IssueSerializer(BaseSerializer): class IssueSerializer(BaseSerializer):
assignees = serializers.ListField( 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) class Meta:
module = ModuleSerializer(source="issue_module.module", many=True) 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: class Meta:
model = Issue model = Issue

View File

@ -21,7 +21,6 @@ class ModuleSerializer(BaseSerializer):
write_only=True, write_only=True,
required=False, required=False,
) )
is_favorite = serializers.BooleanField(read_only=True)
total_issues = serializers.IntegerField(read_only=True) total_issues = serializers.IntegerField(read_only=True)
cancelled_issues = serializers.IntegerField(read_only=True) cancelled_issues = serializers.IntegerField(read_only=True)
completed_issues = serializers.IntegerField(read_only=True) completed_issues = serializers.IntegerField(read_only=True)
@ -154,3 +153,10 @@ class ModuleLinkSerializer(BaseSerializer):
{"error": "URL already exists for this Issue"} {"error": "URL already exists for this Issue"}
) )
return ModuleLink.objects.create(**validated_data) return ModuleLink.objects.create(**validated_data)
class ModuleLiteSerializer(BaseSerializer):
class Meta:
model = Module
fields = "__all__"

View File

@ -52,7 +52,7 @@ class WebhookMixin:
# Push the object to delay # Push the object to delay
send_webhook.delay( send_webhook.delay(
event=self.webhook_event, event=self.webhook_event,
event_data=response.data, payload=response.data,
kw=self.kwargs, kw=self.kwargs,
action=self.request.method, action=self.request.method,
slug=self.workspace_slug, slug=self.workspace_slug,

View File

@ -311,6 +311,7 @@ class CycleIssueAPIEndpoint(WebhookMixin, BaseAPIView):
serializer_class = CycleIssueSerializer serializer_class = CycleIssueSerializer
model = CycleIssue model = CycleIssue
webhook_event = "cycle_issue" webhook_event = "cycle_issue"
bulk = True
permission_classes = [ permission_classes = [
ProjectEntityPermission, ProjectEntityPermission,
] ]

View File

@ -43,27 +43,28 @@ class TimezoneMixin:
class WebhookMixin: class WebhookMixin:
webhook_event = None webhook_event = None
bulk = False
def finalize_response(self, request, response, *args, **kwargs): def finalize_response(self, request, response, *args, **kwargs):
response = super().finalize_response(request, response, *args, **kwargs) response = super().finalize_response(request, response, *args, **kwargs)
# Check for the case should webhook be sent
if ( if (
self.webhook_event 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] and response.status_code in [200, 201, 204]
): ):
# Get the id # Push the object to delay
object_id = (
response.data.get("id") if isinstance(response.data, dict) else None
)
send_webhook.delay( send_webhook.delay(
event=self.webhook_event, event=self.webhook_event,
event_id=object_id, payload=response.data,
kw=self.kwargs,
action=self.request.method, action=self.request.method,
slug=self.workspace_slug, slug=self.workspace_slug,
bulk=self.bulk,
) )
return response
return response
class BaseViewSet(TimezoneMixin, ModelViewSet, BasePaginator): class BaseViewSet(TimezoneMixin, ModelViewSet, BasePaginator):

View File

@ -502,7 +502,10 @@ class CycleViewSet(WebhookMixin, BaseViewSet):
class CycleIssueViewSet(WebhookMixin, BaseViewSet): class CycleIssueViewSet(WebhookMixin, BaseViewSet):
serializer_class = CycleIssueSerializer serializer_class = CycleIssueSerializer
model = CycleIssue model = CycleIssue
webhook_event = "cycle_issue" webhook_event = "cycle_issue"
bulk = True
permission_classes = [ permission_classes = [
ProjectEntityPermission, ProjectEntityPermission,
] ]

View File

@ -287,6 +287,8 @@ class ModuleIssueViewSet(WebhookMixin, BaseViewSet):
serializer_class = ModuleIssueSerializer serializer_class = ModuleIssueSerializer
model = ModuleIssue model = ModuleIssue
webhook_event = "module_issue" webhook_event = "module_issue"
bulk = True
filterset_fields = [ filterset_fields = [
"issue__labels__id", "issue__labels__id",

View File

@ -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) 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) serializer = SERIALIZER_MAPPER.get(event)
return serializer(queryset).data return serializer(queryset, many=many).data
@shared_task( @shared_task(
@ -89,11 +92,11 @@ def webhook_task(self, webhook, slug, event, event_data, action):
# Use HMAC for generating signature # Use HMAC for generating signature
if webhook.secret_key: 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( hmac_signature = hmac.new(
webhook.secret_key.encode("utf-8"), webhook.secret_key.encode("utf-8"),
event_data_json.encode("utf-8"), event_data_json.encode("utf-8"),
hashlib.sha256 hashlib.sha256,
) )
signature = hmac_signature.hexdigest() signature = hmac_signature.hexdigest()
headers["X-Plane-Signature"] = signature headers["X-Plane-Signature"] = signature
@ -157,7 +160,6 @@ def webhook_task(self, webhook, slug, event, event_data, action):
raise requests.RequestException() raise requests.RequestException()
except Exception as e: except Exception as e:
print(e)
if settings.DEBUG: if settings.DEBUG:
print(e) print(e)
capture_exception(e) capture_exception(e)
@ -165,7 +167,7 @@ def webhook_task(self, webhook, slug, event, event_data, action):
@shared_task() @shared_task()
def send_webhook(event, event_data, kw, action, slug, bulk): def send_webhook(event, payload, kw, action, slug, bulk):
try: try:
webhooks = Webhook.objects.filter(workspace__slug=slug, is_active=True) 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": if event == "issue_comment":
webhooks = webhooks.filter(issue_comment=True) webhooks = webhooks.filter(issue_comment=True)
if webhooks: if webhooks:
if action in ["POST", "PATCH"]: if action in ["POST", "PATCH"]:
if bulk: if bulk and event in ["cycle_issue", "module_issue"]:
event_data= [] event_data = IssueExpandSerializer(
if event in ["cycle_issue", "module_issue"]: Issue.objects.filter(
pass pk__in=[
str(event.get("issue")) for event in payload
]
).prefetch_related("issue_cycle", "issue_module"), many=True
).data
event = "issue"
action = "PATCH"
else: else:
event_id = event_data.get("id") if isinstance(event_data, dict) else None event_data = [
event_data = [get_model_data(event=event, event_id=event_id, many=isinstance(event_id, list))] get_model_data(
event=event,
event_id=payload.get("id") if isinstance(payload, dict) else None,
many=False,
)
]
if action == "DELETE": if action == "DELETE":
event_data = [{"id": kw.get("pk")}] event_data = [{"id": kw.get("pk")}]
for webhook in webhooks: for webhook in webhooks:
for data in event_data: 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: except Exception as e:
if settings.DEBUG: if settings.DEBUG:

View File

@ -51,9 +51,9 @@ class Module(ProjectBaseModel):
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
if self._state.adding: if self._state.adding:
smallest_sort_order = Module.objects.filter( smallest_sort_order = Module.objects.filter(project=self.project).aggregate(
project=self.project smallest=models.Min("sort_order")
).aggregate(smallest=models.Min("sort_order"))["smallest"] )["smallest"]
if smallest_sort_order is not None: if smallest_sort_order is not None:
self.sort_order = smallest_sort_order - 10000 self.sort_order = smallest_sort_order - 10000