forked from github/plane
chore: api and webhook refactor (#2861)
* chore: bug fix * dev: changes in api endpoints for invitations and inbox * chore: improvements * dev: update webhook send * dev: webhook validation and fix webhook flow for app * dev: error messages for deactivation * chore: api fixes * dev: update webhook and workspace leave * chore: issue comment * dev: default values for environment variables * dev: make the user active if he was already part of project member * chore: webhook cycle and module event * dev: disable ssl for emails * dev: webhooks restructuring * dev: updated webhook configuration * dev: webhooks * dev: state get object * dev: update workspace slug validation * dev: remove deactivation flag if max retries exceeded --------- Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
This commit is contained in:
parent
8d76c96a6f
commit
a6d5eab634
@ -9,8 +9,9 @@ from .issue import (
|
||||
IssueCommentSerializer,
|
||||
IssueAttachmentSerializer,
|
||||
IssueActivitySerializer,
|
||||
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
|
@ -46,4 +46,11 @@ class CycleIssueSerializer(BaseSerializer):
|
||||
"workspace",
|
||||
"project",
|
||||
"cycle",
|
||||
]
|
||||
]
|
||||
|
||||
|
||||
class CycleLiteSerializer(BaseSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Cycle
|
||||
fields = "__all__"
|
@ -8,6 +8,12 @@ class InboxIssueSerializer(BaseSerializer):
|
||||
model = InboxIssue
|
||||
fields = "__all__"
|
||||
read_only_fields = [
|
||||
"project",
|
||||
"id",
|
||||
"workspace",
|
||||
"project",
|
||||
"issue",
|
||||
"created_by",
|
||||
"updated_by",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
]
|
@ -19,6 +19,8 @@ from plane.db.models import (
|
||||
ProjectMember,
|
||||
)
|
||||
from .base import BaseSerializer
|
||||
from .cycle import CycleSerializer, CycleLiteSerializer
|
||||
from .module import ModuleSerializer, ModuleLiteSerializer
|
||||
|
||||
|
||||
class IssueSerializer(BaseSerializer):
|
||||
@ -42,6 +44,7 @@ class IssueSerializer(BaseSerializer):
|
||||
model = Issue
|
||||
fields = "__all__"
|
||||
read_only_fields = [
|
||||
"id",
|
||||
"workspace",
|
||||
"project",
|
||||
"created_by",
|
||||
@ -60,9 +63,9 @@ class IssueSerializer(BaseSerializer):
|
||||
|
||||
# Validate assignees are from project
|
||||
if data.get("assignees", []):
|
||||
print(data.get("assignees"))
|
||||
data["assignees"] = ProjectMember.objects.filter(
|
||||
project_id=self.context.get("project_id"),
|
||||
is_active=True,
|
||||
member_id__in=data["assignees"],
|
||||
).values_list("member_id", flat=True)
|
||||
|
||||
@ -88,7 +91,7 @@ class IssueSerializer(BaseSerializer):
|
||||
if (
|
||||
data.get("parent")
|
||||
and not Issue.objects.filter(
|
||||
workspce_id=self.context.get("workspace_id"), pk=data.get("parent")
|
||||
workspace_id=self.context.get("workspace_id"), pk=data.get("parent")
|
||||
).exists()
|
||||
):
|
||||
raise serializers.ValidationError(
|
||||
@ -231,8 +234,13 @@ class LabelSerializer(BaseSerializer):
|
||||
model = Label
|
||||
fields = "__all__"
|
||||
read_only_fields = [
|
||||
"id",
|
||||
"workspace",
|
||||
"project",
|
||||
"created_by",
|
||||
"updated_by",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
]
|
||||
|
||||
|
||||
@ -241,13 +249,14 @@ class IssueLinkSerializer(BaseSerializer):
|
||||
model = IssueLink
|
||||
fields = "__all__"
|
||||
read_only_fields = [
|
||||
"id",
|
||||
"workspace",
|
||||
"project",
|
||||
"issue",
|
||||
"created_by",
|
||||
"updated_by",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"issue",
|
||||
]
|
||||
|
||||
# Validation if url already exists
|
||||
@ -266,13 +275,14 @@ class IssueAttachmentSerializer(BaseSerializer):
|
||||
model = IssueAttachment
|
||||
fields = "__all__"
|
||||
read_only_fields = [
|
||||
"id",
|
||||
"workspace",
|
||||
"project",
|
||||
"issue",
|
||||
"created_by",
|
||||
"updated_by",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"workspace",
|
||||
"project",
|
||||
"issue",
|
||||
]
|
||||
|
||||
|
||||
@ -282,38 +292,61 @@ class IssueCommentSerializer(BaseSerializer):
|
||||
class Meta:
|
||||
model = IssueComment
|
||||
fields = "__all__"
|
||||
read_only_fields = [
|
||||
"workspace",
|
||||
"project",
|
||||
"issue",
|
||||
"created_by",
|
||||
"updated_by",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
]
|
||||
|
||||
|
||||
class IssueAttachmentSerializer(BaseSerializer):
|
||||
class Meta:
|
||||
model = IssueAttachment
|
||||
fields = "__all__"
|
||||
read_only_fields = [
|
||||
"id",
|
||||
"workspace",
|
||||
"project",
|
||||
"issue",
|
||||
"created_by",
|
||||
"updated_by",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"workspace",
|
||||
"project",
|
||||
"issue",
|
||||
]
|
||||
|
||||
|
||||
class IssueActivitySerializer(BaseSerializer):
|
||||
class Meta:
|
||||
model = IssueActivity
|
||||
fields = "__all__"
|
||||
exclude = [
|
||||
"created_by",
|
||||
"udpated_by",
|
||||
"updated_by",
|
||||
]
|
||||
|
||||
|
||||
class CycleIssueSerializer(BaseSerializer):
|
||||
cycle = CycleSerializer(read_only=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
|
||||
fields = "__all__"
|
||||
read_only_fields = [
|
||||
"id",
|
||||
"workspace",
|
||||
"project",
|
||||
"created_by",
|
||||
"updated_by",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
]
|
@ -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)
|
||||
@ -33,6 +32,7 @@ class ModuleSerializer(BaseSerializer):
|
||||
model = Module
|
||||
fields = "__all__"
|
||||
read_only_fields = [
|
||||
"id",
|
||||
"workspace",
|
||||
"project",
|
||||
"created_by",
|
||||
@ -152,4 +152,11 @@ class ModuleLinkSerializer(BaseSerializer):
|
||||
raise serializers.ValidationError(
|
||||
{"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__"
|
@ -20,8 +20,12 @@ class ProjectSerializer(BaseSerializer):
|
||||
model = Project
|
||||
fields = "__all__"
|
||||
read_only_fields = [
|
||||
"workspace",
|
||||
"id",
|
||||
"workspace",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"created_by",
|
||||
"updated_by",
|
||||
]
|
||||
|
||||
def validate(self, data):
|
||||
|
@ -13,7 +13,7 @@ urlpatterns = [
|
||||
name="cycles",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/cycles/<uuid:cycle_id>/",
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/cycles/<uuid:pk>/",
|
||||
CycleAPIEndpoint.as_view(),
|
||||
name="cycles",
|
||||
),
|
||||
@ -23,7 +23,7 @@ urlpatterns = [
|
||||
name="cycle-issues",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/cycles/<uuid:cycle_id>/cycle-issues/<uuid:pk>/",
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/cycles/<uuid:cycle_id>/cycle-issues/<uuid:issue_id>/",
|
||||
CycleIssueAPIEndpoint.as_view(),
|
||||
name="cycle-issues",
|
||||
),
|
||||
|
@ -5,12 +5,12 @@ from plane.api.views import InboxIssueAPIEndpoint
|
||||
|
||||
urlpatterns = [
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/inboxes/<uuid:inbox_id>/inbox-issues/",
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/inbox-issues/",
|
||||
InboxIssueAPIEndpoint.as_view(),
|
||||
name="inbox-issue",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/inboxes/<uuid:inbox_id>/inbox-issues/<uuid:pk>/",
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/inbox-issues/<uuid:pk>/",
|
||||
InboxIssueAPIEndpoint.as_view(),
|
||||
name="inbox-issue",
|
||||
),
|
||||
|
@ -15,27 +15,27 @@ urlpatterns = [
|
||||
name="issue",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/",
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:pk>/",
|
||||
IssueAPIEndpoint.as_view(),
|
||||
name="issue",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/issue-labels/",
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/labels/",
|
||||
LabelAPIEndpoint.as_view(),
|
||||
name="label",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/issue-labels/<uuid:pk>/",
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/labels/<uuid:pk>/",
|
||||
LabelAPIEndpoint.as_view(),
|
||||
name="label",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/issue-links/",
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/links/",
|
||||
IssueLinkAPIEndpoint.as_view(),
|
||||
name="link",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/issue-links/<uuid:pk>/",
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/links/<uuid:pk>/",
|
||||
IssueLinkAPIEndpoint.as_view(),
|
||||
name="link",
|
||||
),
|
||||
@ -50,12 +50,12 @@ urlpatterns = [
|
||||
name="comment",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/activites/",
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/activities/",
|
||||
IssueActivityAPIEndpoint.as_view(),
|
||||
name="activity",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/activites/<uuid:pk>/",
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/activities/<uuid:pk>/",
|
||||
IssueActivityAPIEndpoint.as_view(),
|
||||
name="activity",
|
||||
),
|
||||
|
@ -19,7 +19,7 @@ urlpatterns = [
|
||||
name="module-issues",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/modules/<uuid:module_id>/module-issues/<uuid:pk>/",
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/modules/<uuid:module_id>/module-issues/<uuid:issue_id>/",
|
||||
ModuleIssueAPIEndpoint.as_view(),
|
||||
name="module-issues",
|
||||
),
|
||||
|
@ -9,7 +9,7 @@ urlpatterns = [
|
||||
name="project",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:pk>/",
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/",
|
||||
ProjectAPIEndpoint.as_view(),
|
||||
name="project",
|
||||
),
|
||||
|
@ -8,4 +8,9 @@ urlpatterns = [
|
||||
StateAPIEndpoint.as_view(),
|
||||
name="states",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/states/<uuid:state_id>/",
|
||||
StateAPIEndpoint.as_view(),
|
||||
name="states",
|
||||
),
|
||||
]
|
@ -7,7 +7,6 @@ from django.conf import settings
|
||||
from django.db import IntegrityError
|
||||
from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
||||
from django.utils import timezone
|
||||
from django.core.serializers.json import DjangoJSONEncoder
|
||||
|
||||
# Third party imports
|
||||
from rest_framework.views import APIView
|
||||
@ -36,28 +35,33 @@ class TimezoneMixin:
|
||||
else:
|
||||
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,
|
||||
event_data=json.dumps(response.data, cls=DjangoJSONEncoder),
|
||||
payload=response.data,
|
||||
kw=self.kwargs,
|
||||
action=self.request.method,
|
||||
slug=self.workspace_slug,
|
||||
bulk=self.bulk,
|
||||
)
|
||||
|
||||
return response
|
||||
|
||||
|
||||
|
||||
class BaseAPIView(TimezoneMixin, APIView, BasePaginator):
|
||||
authentication_classes = [
|
||||
APIKeyAuthentication,
|
||||
@ -139,13 +143,13 @@ class BaseAPIView(TimezoneMixin, APIView, BasePaginator):
|
||||
response = super().finalize_response(request, response, *args, **kwargs)
|
||||
|
||||
# Add custom headers if they exist in the request META
|
||||
ratelimit_remaining = request.META.get('X-RateLimit-Remaining')
|
||||
ratelimit_remaining = request.META.get("X-RateLimit-Remaining")
|
||||
if ratelimit_remaining is not None:
|
||||
response['X-RateLimit-Remaining'] = ratelimit_remaining
|
||||
response["X-RateLimit-Remaining"] = ratelimit_remaining
|
||||
|
||||
ratelimit_reset = request.META.get('X-RateLimit-Reset')
|
||||
ratelimit_reset = request.META.get("X-RateLimit-Reset")
|
||||
if ratelimit_reset is not None:
|
||||
response['X-RateLimit-Reset'] = ratelimit_reset
|
||||
response["X-RateLimit-Reset"] = ratelimit_reset
|
||||
|
||||
return response
|
||||
|
||||
@ -169,4 +173,4 @@ class BaseAPIView(TimezoneMixin, APIView, BasePaginator):
|
||||
expand = [
|
||||
expand for expand in self.request.GET.get("expand", "").split(",") if expand
|
||||
]
|
||||
return expand if expand else None
|
||||
return expand if expand else None
|
||||
|
@ -17,7 +17,6 @@ from plane.app.permissions import ProjectEntityPermission
|
||||
from plane.api.serializers import (
|
||||
CycleSerializer,
|
||||
CycleIssueSerializer,
|
||||
IssueSerializer,
|
||||
)
|
||||
from plane.bgtasks.issue_activites_task import issue_activity
|
||||
|
||||
@ -142,7 +141,6 @@ class CycleAPIEndpoint(WebhookMixin, BaseAPIView):
|
||||
)
|
||||
queryset = self.get_queryset()
|
||||
cycle_view = request.GET.get("cycle_view", "all")
|
||||
queryset = queryset.order_by("-is_favorite", "-created_at")
|
||||
|
||||
# Current Cycle
|
||||
if cycle_view == "current":
|
||||
@ -293,7 +291,7 @@ class CycleAPIEndpoint(WebhookMixin, BaseAPIView):
|
||||
}
|
||||
),
|
||||
actor_id=str(request.user.id),
|
||||
issue_id=str(pk),
|
||||
issue_id=None,
|
||||
project_id=str(project_id),
|
||||
current_instance=None,
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
@ -305,14 +303,15 @@ class CycleAPIEndpoint(WebhookMixin, BaseAPIView):
|
||||
|
||||
class CycleIssueAPIEndpoint(WebhookMixin, BaseAPIView):
|
||||
"""
|
||||
This viewset automatically provides `list`, `create`, `retrieve`,
|
||||
`update` and `destroy` actions related to cycle issues.
|
||||
This viewset automatically provides `list`, `create`,
|
||||
and `destroy` actions related to cycle issues.
|
||||
|
||||
"""
|
||||
|
||||
serializer_class = CycleIssueSerializer
|
||||
model = CycleIssue
|
||||
webhook_event = "cycle"
|
||||
webhook_event = "cycle_issue"
|
||||
bulk = True
|
||||
permission_classes = [
|
||||
ProjectEntityPermission,
|
||||
]
|
||||
@ -457,7 +456,7 @@ class CycleIssueAPIEndpoint(WebhookMixin, BaseAPIView):
|
||||
# Capture Issue Activity
|
||||
issue_activity.delay(
|
||||
type="cycle.activity.created",
|
||||
requested_data=json.dumps({"cycles_list": issues}),
|
||||
requested_data=json.dumps({"cycles_list": str(issues)}),
|
||||
actor_id=str(self.request.user.id),
|
||||
issue_id=None,
|
||||
project_id=str(self.kwargs.get("project_id", None)),
|
||||
@ -478,9 +477,9 @@ class CycleIssueAPIEndpoint(WebhookMixin, BaseAPIView):
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
def delete(self, request, slug, project_id, cycle_id, pk):
|
||||
def delete(self, request, slug, project_id, cycle_id, issue_id):
|
||||
cycle_issue = CycleIssue.objects.get(
|
||||
pk=pk, workspace__slug=slug, project_id=project_id, cycle_id=cycle_id
|
||||
issue_id=issue_id, workspace__slug=slug, project_id=project_id, cycle_id=cycle_id
|
||||
)
|
||||
issue_id = cycle_issue.issue_id
|
||||
cycle_issue.delete()
|
||||
@ -493,7 +492,7 @@ class CycleIssueAPIEndpoint(WebhookMixin, BaseAPIView):
|
||||
}
|
||||
),
|
||||
actor_id=str(self.request.user.id),
|
||||
issue_id=str(self.kwargs.get("pk", None)),
|
||||
issue_id=str(issue_id),
|
||||
project_id=str(self.kwargs.get("project_id", None)),
|
||||
current_instance=None,
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
|
@ -14,7 +14,7 @@ from rest_framework.response import Response
|
||||
from .base import BaseAPIView
|
||||
from plane.app.permissions import ProjectLitePermission
|
||||
from plane.api.serializers import InboxIssueSerializer, IssueSerializer
|
||||
from plane.db.models import InboxIssue, Issue, State, ProjectMember
|
||||
from plane.db.models import InboxIssue, Issue, State, ProjectMember, Project, Inbox
|
||||
from plane.bgtasks.issue_activites_task import issue_activity
|
||||
|
||||
|
||||
@ -37,29 +37,39 @@ class InboxIssueAPIEndpoint(BaseAPIView):
|
||||
]
|
||||
|
||||
def get_queryset(self):
|
||||
return self.filter_queryset(
|
||||
super()
|
||||
.get_queryset()
|
||||
.filter(
|
||||
inbox = Inbox.objects.filter(
|
||||
workspace__slug=self.kwargs.get("slug"),
|
||||
project_id=self.kwargs.get("project_id"),
|
||||
).first()
|
||||
|
||||
project = Project.objects.get(
|
||||
workspace__slug=self.kwargs.get("slug"), pk=self.kwargs.get("project_id")
|
||||
)
|
||||
|
||||
if inbox is None and not project.inbox_view:
|
||||
return InboxIssue.objects.none()
|
||||
|
||||
return (
|
||||
InboxIssue.objects.filter(
|
||||
Q(snoozed_till__gte=timezone.now()) | Q(snoozed_till__isnull=True),
|
||||
workspace__slug=self.kwargs.get("slug"),
|
||||
project_id=self.kwargs.get("project_id"),
|
||||
inbox_id=self.kwargs.get("inbox_id"),
|
||||
inbox_id=inbox.id,
|
||||
)
|
||||
.select_related("issue", "workspace", "project")
|
||||
.order_by(self.kwargs.get("order_by", "-created_at"))
|
||||
)
|
||||
|
||||
def get(self, request, slug, project_id, inbox_id, pk=None):
|
||||
def get(self, request, slug, project_id, pk=None):
|
||||
if pk:
|
||||
issue_queryset = self.get_queryset().get(pk=pk)
|
||||
issues_data = InboxIssueSerializer(
|
||||
issue_queryset,
|
||||
inbox_issue_queryset = self.get_queryset().get(pk=pk)
|
||||
inbox_issue_data = InboxIssueSerializer(
|
||||
inbox_issue_queryset,
|
||||
fields=self.fields,
|
||||
expand=self.expand,
|
||||
).data
|
||||
return Response(
|
||||
issues_data,
|
||||
inbox_issue_data,
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
issue_queryset = self.get_queryset()
|
||||
@ -74,12 +84,30 @@ class InboxIssueAPIEndpoint(BaseAPIView):
|
||||
).data,
|
||||
)
|
||||
|
||||
def post(self, request, slug, project_id, inbox_id):
|
||||
def post(self, request, slug, project_id):
|
||||
if not request.data.get("issue", {}).get("name", False):
|
||||
return Response(
|
||||
{"error": "Name is required"}, status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
inbox = Inbox.objects.filter(
|
||||
workspace__slug=slug, project_id=project_id
|
||||
).first()
|
||||
|
||||
project = Project.objects.get(
|
||||
workspace__slug=slug,
|
||||
pk=project_id,
|
||||
)
|
||||
|
||||
# Inbox view
|
||||
if inbox is None and not project.inbox_view:
|
||||
return Response(
|
||||
{
|
||||
"error": "Inbox is not enabled for this project enable it through the project settings"
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
# Check for valid priority
|
||||
if not request.data.get("issue", {}).get("priority", "none") in [
|
||||
"low",
|
||||
@ -123,21 +151,45 @@ class InboxIssueAPIEndpoint(BaseAPIView):
|
||||
current_instance=None,
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
)
|
||||
|
||||
# create an inbox issue
|
||||
InboxIssue.objects.create(
|
||||
inbox_id=inbox_id,
|
||||
inbox_issue = InboxIssue.objects.create(
|
||||
inbox_id=inbox.id,
|
||||
project_id=project_id,
|
||||
issue=issue,
|
||||
source=request.data.get("source", "in-app"),
|
||||
)
|
||||
|
||||
serializer = IssueSerializer(issue)
|
||||
serializer = InboxIssueSerializer(inbox_issue)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
def patch(self, request, slug, project_id, inbox_id, pk):
|
||||
inbox_issue = InboxIssue.objects.get(
|
||||
pk=pk, workspace__slug=slug, project_id=project_id, inbox_id=inbox_id
|
||||
def patch(self, request, slug, project_id, pk):
|
||||
inbox = Inbox.objects.filter(
|
||||
workspace__slug=slug, project_id=project_id
|
||||
).first()
|
||||
|
||||
project = Project.objects.get(
|
||||
workspace__slug=slug,
|
||||
pk=project_id,
|
||||
)
|
||||
|
||||
# Inbox view
|
||||
if inbox is None and not project.inbox_view:
|
||||
return Response(
|
||||
{
|
||||
"error": "Inbox is not enabled for this project enable it through the project settings"
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
# Get the inbox issue
|
||||
inbox_issue = InboxIssue.objects.get(
|
||||
pk=pk,
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
inbox_id=inbox.id,
|
||||
)
|
||||
|
||||
# Get the project member
|
||||
project_member = ProjectMember.objects.get(
|
||||
workspace__slug=slug,
|
||||
@ -145,6 +197,7 @@ class InboxIssueAPIEndpoint(BaseAPIView):
|
||||
member=request.user,
|
||||
is_active=True,
|
||||
)
|
||||
|
||||
# Only project members admins and created_by users can access this endpoint
|
||||
if project_member.role <= 10 and str(inbox_issue.created_by_id) != str(
|
||||
request.user.id
|
||||
@ -244,10 +297,33 @@ class InboxIssueAPIEndpoint(BaseAPIView):
|
||||
InboxIssueSerializer(inbox_issue).data, status=status.HTTP_200_OK
|
||||
)
|
||||
|
||||
def delete(self, request, slug, project_id, inbox_id, pk):
|
||||
inbox_issue = InboxIssue.objects.get(
|
||||
pk=pk, workspace__slug=slug, project_id=project_id, inbox_id=inbox_id
|
||||
def delete(self, request, slug, project_id, pk):
|
||||
inbox = Inbox.objects.filter(
|
||||
workspace__slug=slug, project_id=project_id
|
||||
).first()
|
||||
|
||||
project = Project.objects.get(
|
||||
workspace__slug=slug,
|
||||
pk=project_id,
|
||||
)
|
||||
|
||||
# Inbox view
|
||||
if inbox is None and not project.inbox_view:
|
||||
return Response(
|
||||
{
|
||||
"error": "Inbox is not enabled for this project enable it through the project settings"
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
# Get the inbox issue
|
||||
inbox_issue = InboxIssue.objects.get(
|
||||
pk=pk,
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
inbox_id=inbox.id,
|
||||
)
|
||||
|
||||
# Get the project member
|
||||
project_member = ProjectMember.objects.get(
|
||||
workspace__slug=slug,
|
||||
@ -256,6 +332,7 @@ class InboxIssueAPIEndpoint(BaseAPIView):
|
||||
is_active=True,
|
||||
)
|
||||
|
||||
# Check the inbox issue created
|
||||
if project_member.role <= 10 and str(inbox_issue.created_by_id) != str(
|
||||
request.user.id
|
||||
):
|
||||
@ -272,4 +349,4 @@ class InboxIssueAPIEndpoint(BaseAPIView):
|
||||
).delete()
|
||||
|
||||
inbox_issue.delete()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
@ -22,7 +22,6 @@ from django.utils import timezone
|
||||
# Third party imports
|
||||
from rest_framework import status
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.parsers import MultiPartParser, FormParser
|
||||
|
||||
# Module imports
|
||||
from .base import BaseAPIView, WebhookMixin
|
||||
@ -41,14 +40,12 @@ from plane.db.models import (
|
||||
IssueComment,
|
||||
IssueActivity,
|
||||
)
|
||||
from plane.utils.issue_filters import issue_filters
|
||||
from plane.bgtasks.issue_activites_task import issue_activity
|
||||
from plane.api.serializers import (
|
||||
IssueSerializer,
|
||||
LabelSerializer,
|
||||
IssueLinkSerializer,
|
||||
IssueCommentSerializer,
|
||||
IssueAttachmentSerializer,
|
||||
IssueActivitySerializer,
|
||||
)
|
||||
|
||||
@ -103,7 +100,6 @@ class IssueAPIEndpoint(WebhookMixin, BaseAPIView):
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
filters = issue_filters(request.query_params, "GET")
|
||||
# Custom ordering for priority and state
|
||||
priority_order = ["urgent", "high", "medium", "low", "none"]
|
||||
state_order = ["backlog", "unstarted", "started", "completed", "cancelled"]
|
||||
@ -112,7 +108,6 @@ class IssueAPIEndpoint(WebhookMixin, BaseAPIView):
|
||||
|
||||
issue_queryset = (
|
||||
self.get_queryset()
|
||||
.filter(**filters)
|
||||
.annotate(cycle_id=F("issue_cycle__cycle_id"))
|
||||
.annotate(module_id=F("issue_module__module_id"))
|
||||
.annotate(
|
||||
@ -278,7 +273,7 @@ class LabelAPIEndpoint(BaseAPIView):
|
||||
|
||||
def get_queryset(self):
|
||||
return (
|
||||
Project.objects.filter(workspace__slug=self.kwargs.get("slug"))
|
||||
Label.objects.filter(workspace__slug=self.kwargs.get("slug"))
|
||||
.filter(project_id=self.kwargs.get("project_id"))
|
||||
.filter(project__project_projectmember__member=self.request.user)
|
||||
.select_related("project")
|
||||
@ -302,29 +297,29 @@ class LabelAPIEndpoint(BaseAPIView):
|
||||
)
|
||||
|
||||
def get(self, request, slug, project_id, pk=None):
|
||||
if pk:
|
||||
label = self.get_queryset().get(pk=pk)
|
||||
serializer = LabelSerializer(
|
||||
label,
|
||||
fields=self.fields,
|
||||
expand=self.expand,
|
||||
if pk is None:
|
||||
return self.paginate(
|
||||
request=request,
|
||||
queryset=(self.get_queryset()),
|
||||
on_results=lambda labels: LabelSerializer(
|
||||
labels,
|
||||
many=True,
|
||||
fields=self.fields,
|
||||
expand=self.expand,
|
||||
).data,
|
||||
)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
return self.paginate(
|
||||
request=request,
|
||||
queryset=(self.get_queryset()),
|
||||
on_results=lambda labels: LabelSerializer(
|
||||
labels,
|
||||
many=True,
|
||||
fields=self.fields,
|
||||
expand=self.expand,
|
||||
).data,
|
||||
)
|
||||
label = self.get_queryset().get(pk=pk)
|
||||
serializer = LabelSerializer(label, fields=self.fields, expand=self.expand,)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
def patch(self, request, slug, project_id, pk=None):
|
||||
label = self.get_queryset().get(pk=pk)
|
||||
serializer = LabelSerializer(label, data=request.data, partial=True)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
|
||||
def delete(self, request, slug, project_id, pk=None):
|
||||
label = self.get_queryset().get(pk=pk)
|
||||
@ -356,25 +351,31 @@ class IssueLinkAPIEndpoint(BaseAPIView):
|
||||
.distinct()
|
||||
)
|
||||
|
||||
def get(self, request, slug, project_id, pk=None):
|
||||
if pk:
|
||||
label = self.get_queryset().get(pk=pk)
|
||||
def get(self, request, slug, project_id, issue_id, pk=None):
|
||||
if pk is None:
|
||||
issue_links = self.get_queryset()
|
||||
serializer = IssueLinkSerializer(
|
||||
label,
|
||||
issue_links,
|
||||
fields=self.fields,
|
||||
expand=self.expand,
|
||||
)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
return self.paginate(
|
||||
request=request,
|
||||
queryset=(self.get_queryset()),
|
||||
on_results=lambda issue_links: IssueLinkSerializer(
|
||||
issue_links,
|
||||
many=True,
|
||||
fields=self.fields,
|
||||
expand=self.expand,
|
||||
).data,
|
||||
return self.paginate(
|
||||
request=request,
|
||||
queryset=(self.get_queryset()),
|
||||
on_results=lambda issue_links: IssueLinkSerializer(
|
||||
issue_links,
|
||||
many=True,
|
||||
fields=self.fields,
|
||||
expand=self.expand,
|
||||
).data,
|
||||
)
|
||||
issue_link = self.get_queryset().get(pk=pk)
|
||||
serializer = IssueLinkSerializer(
|
||||
issue_link,
|
||||
fields=self.fields,
|
||||
expand=self.expand,
|
||||
)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
def post(self, request, slug, project_id, issue_id):
|
||||
serializer = IssueLinkSerializer(data=request.data)
|
||||
@ -449,7 +450,7 @@ class IssueCommentAPIEndpoint(WebhookMixin, BaseAPIView):
|
||||
|
||||
serializer_class = IssueCommentSerializer
|
||||
model = IssueComment
|
||||
webhook_event = "issue-comment"
|
||||
webhook_event = "issue_comment"
|
||||
permission_classes = [
|
||||
ProjectLitePermission,
|
||||
]
|
||||
@ -587,7 +588,7 @@ class IssueActivityAPIEndpoint(BaseAPIView):
|
||||
serializer = IssueActivitySerializer(issue_activities)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
self.paginate(
|
||||
return self.paginate(
|
||||
request=request,
|
||||
queryset=(issue_activities),
|
||||
on_results=lambda issue_activity: IssueActivitySerializer(
|
||||
|
@ -129,6 +129,14 @@ class ModuleAPIEndpoint(WebhookMixin, BaseAPIView):
|
||||
serializer = ModuleSerializer(module)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
def patch(self, request, slug, project_id, pk):
|
||||
module = Module.objects.get(pk=pk, project_id=project_id, workspace__slug=slug)
|
||||
serializer = ModuleSerializer(module, data=request.data)
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
def get(self, request, slug, project_id, pk=None):
|
||||
if pk:
|
||||
@ -168,7 +176,7 @@ class ModuleAPIEndpoint(WebhookMixin, BaseAPIView):
|
||||
}
|
||||
),
|
||||
actor_id=str(request.user.id),
|
||||
issue_id=str(pk),
|
||||
issue_id=None,
|
||||
project_id=str(project_id),
|
||||
current_instance=None,
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
@ -186,7 +194,8 @@ class ModuleIssueAPIEndpoint(WebhookMixin, BaseAPIView):
|
||||
|
||||
serializer_class = ModuleIssueSerializer
|
||||
model = ModuleIssue
|
||||
webhook_event = "module"
|
||||
webhook_event = "module_issue"
|
||||
bulk = True
|
||||
|
||||
permission_classes = [
|
||||
ProjectEntityPermission,
|
||||
@ -323,7 +332,7 @@ class ModuleIssueAPIEndpoint(WebhookMixin, BaseAPIView):
|
||||
# Capture Issue Activity
|
||||
issue_activity.delay(
|
||||
type="module.activity.created",
|
||||
requested_data=json.dumps({"modules_list": issues}),
|
||||
requested_data=json.dumps({"modules_list": str(issues)}),
|
||||
actor_id=str(self.request.user.id),
|
||||
issue_id=None,
|
||||
project_id=str(self.kwargs.get("project_id", None)),
|
||||
@ -343,9 +352,9 @@ class ModuleIssueAPIEndpoint(WebhookMixin, BaseAPIView):
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
def delete(self, request, slug, project_id, module_id, pk):
|
||||
def delete(self, request, slug, project_id, module_id, issue_id):
|
||||
module_issue = ModuleIssue.objects.get(
|
||||
workspace__slug=slug, project_id=project_id, module_id=module_id, pk=pk
|
||||
workspace__slug=slug, project_id=project_id, module_id=module_id, issue_id=issue_id
|
||||
)
|
||||
module_issue.delete()
|
||||
issue_activity.delay(
|
||||
@ -357,7 +366,7 @@ class ModuleIssueAPIEndpoint(WebhookMixin, BaseAPIView):
|
||||
}
|
||||
),
|
||||
actor_id=str(request.user.id),
|
||||
issue_id=str(pk),
|
||||
issue_id=str(issue_id),
|
||||
project_id=str(project_id),
|
||||
current_instance=None,
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
|
@ -94,8 +94,8 @@ class ProjectAPIEndpoint(WebhookMixin, BaseAPIView):
|
||||
.distinct()
|
||||
)
|
||||
|
||||
def get(self, request, slug, pk=None):
|
||||
if pk is None:
|
||||
def get(self, request, slug, project_id=None):
|
||||
if project_id is None:
|
||||
sort_order_query = ProjectMember.objects.filter(
|
||||
member=request.user,
|
||||
project_id=OuterRef("pk"),
|
||||
@ -114,7 +114,7 @@ class ProjectAPIEndpoint(WebhookMixin, BaseAPIView):
|
||||
).select_related("member"),
|
||||
)
|
||||
)
|
||||
.order_by("sort_order", "name")
|
||||
.order_by(request.GET.get("order_by", "sort_order"))
|
||||
)
|
||||
return self.paginate(
|
||||
request=request,
|
||||
@ -123,15 +123,13 @@ class ProjectAPIEndpoint(WebhookMixin, BaseAPIView):
|
||||
projects, many=True, fields=self.fields, expand=self.expand,
|
||||
).data,
|
||||
)
|
||||
else:
|
||||
project = self.get_queryset().get(workspace__slug=slug, pk=pk)
|
||||
serializer = ProjectSerializer(project, fields=self.fields, expand=self.expand,)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
project = self.get_queryset().get(workspace__slug=slug, pk=project_id)
|
||||
serializer = ProjectSerializer(project, fields=self.fields, expand=self.expand,)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
def post(self, request, slug):
|
||||
try:
|
||||
workspace = Workspace.objects.get(slug=slug)
|
||||
|
||||
serializer = ProjectSerializer(
|
||||
data={**request.data}, context={"workspace_id": workspace.id}
|
||||
)
|
||||
@ -236,10 +234,10 @@ class ProjectAPIEndpoint(WebhookMixin, BaseAPIView):
|
||||
status=status.HTTP_410_GONE,
|
||||
)
|
||||
|
||||
def patch(self, request, slug, pk=None):
|
||||
def patch(self, request, slug, project_id=None):
|
||||
try:
|
||||
workspace = Workspace.objects.get(slug=slug)
|
||||
project = Project.objects.get(pk=pk)
|
||||
project = Project.objects.get(pk=project_id)
|
||||
|
||||
serializer = ProjectSerializer(
|
||||
project,
|
||||
@ -260,7 +258,7 @@ class ProjectAPIEndpoint(WebhookMixin, BaseAPIView):
|
||||
name="Triage",
|
||||
group="backlog",
|
||||
description="Default state for managing all Inbox Issues",
|
||||
project_id=pk,
|
||||
project_id=project_id,
|
||||
color="#ff7700",
|
||||
)
|
||||
|
||||
@ -282,4 +280,9 @@ class ProjectAPIEndpoint(WebhookMixin, BaseAPIView):
|
||||
return Response(
|
||||
{"identifier": "The project identifier is already taken"},
|
||||
status=status.HTTP_410_GONE,
|
||||
)
|
||||
)
|
||||
|
||||
def delete(self, request, slug, project_id):
|
||||
project = Project.objects.get(pk=project_id, workspace__slug=slug)
|
||||
project.delete()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
@ -23,10 +23,8 @@ class StateAPIEndpoint(BaseAPIView):
|
||||
]
|
||||
|
||||
def get_queryset(self):
|
||||
return self.filter_queryset(
|
||||
super()
|
||||
.get_queryset()
|
||||
.filter(workspace__slug=self.kwargs.get("slug"))
|
||||
return (
|
||||
State.objects.filter(workspace__slug=self.kwargs.get("slug"))
|
||||
.filter(project_id=self.kwargs.get("project_id"))
|
||||
.filter(project__project_projectmember__member=self.request.user)
|
||||
.filter(~Q(name="Triage"))
|
||||
@ -42,9 +40,9 @@ class StateAPIEndpoint(BaseAPIView):
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
def get(self, request, slug, project_id, pk=None):
|
||||
if pk:
|
||||
serializer = StateSerializer(self.get_queryset().get(pk=pk))
|
||||
def get(self, request, slug, project_id, state_id=None):
|
||||
if state_id:
|
||||
serializer = StateSerializer(self.get_queryset().get(pk=state_id))
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
return self.paginate(
|
||||
request=request,
|
||||
@ -57,10 +55,10 @@ class StateAPIEndpoint(BaseAPIView):
|
||||
).data,
|
||||
)
|
||||
|
||||
def delete(self, request, slug, project_id, pk):
|
||||
def delete(self, request, slug, project_id, state_id):
|
||||
state = State.objects.get(
|
||||
~Q(name="Triage"),
|
||||
pk=pk,
|
||||
pk=state_id,
|
||||
project_id=project_id,
|
||||
workspace__slug=slug,
|
||||
)
|
||||
@ -69,7 +67,7 @@ class StateAPIEndpoint(BaseAPIView):
|
||||
return Response({"error": "Default state cannot be deleted"}, status=False)
|
||||
|
||||
# Check for any issues in the state
|
||||
issue_exist = Issue.issue_objects.filter(state=pk).exists()
|
||||
issue_exist = Issue.issue_objects.filter(state=state_id).exists()
|
||||
|
||||
if issue_exist:
|
||||
return Response(
|
||||
@ -80,8 +78,8 @@ class StateAPIEndpoint(BaseAPIView):
|
||||
state.delete()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
def patch(self, request, slug, project_id, pk=None):
|
||||
state = State.objects.filter(workspace__slug=slug, project_id=project_id, pk=pk)
|
||||
def patch(self, request, slug, project_id, state_id=None):
|
||||
state = State.objects.get(workspace__slug=slug, project_id=project_id, pk=state_id)
|
||||
serializer = StateSerializer(state, data=request.data, partial=True)
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
|
@ -1,3 +1,9 @@
|
||||
# Python imports
|
||||
import urllib
|
||||
import socket
|
||||
import ipaddress
|
||||
from urllib.parse import urlparse
|
||||
|
||||
# Third party imports
|
||||
from rest_framework import serializers
|
||||
|
||||
@ -9,6 +15,42 @@ from plane.db.models.webhook import validate_domain, validate_schema
|
||||
class WebhookSerializer(DynamicBaseSerializer):
|
||||
url = serializers.URLField(validators=[validate_schema, validate_domain])
|
||||
|
||||
def validate(self, data):
|
||||
url = data.get("url", None)
|
||||
|
||||
# Extract the hostname from the URL
|
||||
hostname = urlparse(url).hostname
|
||||
if not hostname:
|
||||
raise serializers.ValidationError({"url": "Invalid URL: No hostname found."})
|
||||
|
||||
# Resolve the hostname to IP addresses
|
||||
try:
|
||||
ip_addresses = socket.getaddrinfo(hostname, None)
|
||||
except socket.gaierror:
|
||||
raise serializers.ValidationError({"url": "Hostname could not be resolved."})
|
||||
|
||||
if not ip_addresses:
|
||||
raise serializers.ValidationError({"url": "No IP addresses found for the hostname."})
|
||||
|
||||
for addr in ip_addresses:
|
||||
ip = ipaddress.ip_address(addr[4][0])
|
||||
if ip.is_private or ip.is_loopback:
|
||||
raise serializers.ValidationError({"url": "URL resolves to a blocked IP address."})
|
||||
|
||||
# Additional validation for multiple request domains and their subdomains
|
||||
request = self.context.get('request')
|
||||
disallowed_domains = ['plane.so',] # Add your disallowed domains here
|
||||
if request:
|
||||
request_host = request.get_host().split(':')[0] # Remove port if present
|
||||
disallowed_domains.append(request_host)
|
||||
|
||||
# Check if hostname is a subdomain or exact match of any disallowed domain
|
||||
if any(hostname == domain or hostname.endswith('.' + domain) for domain in disallowed_domains):
|
||||
raise serializers.ValidationError({"url": "URL domain or its subdomain is not allowed."})
|
||||
|
||||
return data
|
||||
|
||||
|
||||
class Meta:
|
||||
model = Webhook
|
||||
fields = "__all__"
|
||||
|
@ -21,6 +21,22 @@ class WorkSpaceSerializer(BaseSerializer):
|
||||
total_members = serializers.IntegerField(read_only=True)
|
||||
total_issues = serializers.IntegerField(read_only=True)
|
||||
|
||||
def validated(self, data):
|
||||
if data.get("slug") in [
|
||||
"404",
|
||||
"accounts",
|
||||
"api",
|
||||
"create-workspace",
|
||||
"god-mode",
|
||||
"installations",
|
||||
"invitations",
|
||||
"onboarding",
|
||||
"profile",
|
||||
"spaces",
|
||||
"workspace-invitations",
|
||||
]:
|
||||
raise serializers.ValidationError({"slug": "Slug is not valid"})
|
||||
|
||||
class Meta:
|
||||
model = Workspace
|
||||
fields = "__all__"
|
||||
|
@ -65,7 +65,7 @@ urlpatterns = [
|
||||
name="project-member-invite",
|
||||
),
|
||||
path(
|
||||
"users/me/invitations/projects/",
|
||||
"users/me/workspaces/<str:slug>/projects/invitations/",
|
||||
UserProjectInvitationsViewset.as_view(
|
||||
{
|
||||
"get": "list",
|
||||
@ -75,7 +75,7 @@ urlpatterns = [
|
||||
name="user-project-invitations",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/join/",
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/join/<uuid:pk>/",
|
||||
ProjectJoinEndpoint.as_view(),
|
||||
name="project-join",
|
||||
),
|
||||
|
@ -43,20 +43,25 @@ 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", "DELETE"]
|
||||
and response.status_code in [200, 201, 204]
|
||||
):
|
||||
# Push the object to delay
|
||||
send_webhook.delay(
|
||||
event=self.webhook_event,
|
||||
event_data=json.dumps(response.data, cls=DjangoJSONEncoder),
|
||||
payload=response.data,
|
||||
kw=self.kwargs,
|
||||
action=self.request.method,
|
||||
slug=self.workspace_slug,
|
||||
bulk=self.bulk,
|
||||
)
|
||||
|
||||
return response
|
||||
|
@ -58,6 +58,7 @@ class ConfigurationEndpoint(BaseAPIView):
|
||||
) and get_configuration_value(
|
||||
instance_configuration, "ENABLE_MAGIC_LINK_LOGIN", "0"
|
||||
) == "1"
|
||||
|
||||
data["email_password_login"] = (
|
||||
get_configuration_value(
|
||||
instance_configuration, "ENABLE_EMAIL_PASSWORD", "0"
|
||||
|
@ -502,7 +502,10 @@ class CycleViewSet(WebhookMixin, BaseViewSet):
|
||||
class CycleIssueViewSet(WebhookMixin, BaseViewSet):
|
||||
serializer_class = CycleIssueSerializer
|
||||
model = CycleIssue
|
||||
webhook_event = "cycle"
|
||||
|
||||
webhook_event = "cycle_issue"
|
||||
bulk = True
|
||||
|
||||
permission_classes = [
|
||||
ProjectEntityPermission,
|
||||
]
|
||||
@ -688,7 +691,6 @@ class CycleIssueViewSet(WebhookMixin, BaseViewSet):
|
||||
pk=pk, workspace__slug=slug, project_id=project_id, cycle_id=cycle_id
|
||||
)
|
||||
issue_id = cycle_issue.issue_id
|
||||
cycle_issue.delete()
|
||||
issue_activity.delay(
|
||||
type="cycle.activity.deleted",
|
||||
requested_data=json.dumps(
|
||||
@ -698,11 +700,12 @@ class CycleIssueViewSet(WebhookMixin, BaseViewSet):
|
||||
}
|
||||
),
|
||||
actor_id=str(self.request.user.id),
|
||||
issue_id=str(self.kwargs.get("pk", None)),
|
||||
issue_id=str(cycle_issue.issue_id),
|
||||
project_id=str(self.kwargs.get("project_id", None)),
|
||||
current_instance=None,
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
)
|
||||
cycle_issue.delete()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
# Python imports
|
||||
import requests
|
||||
|
||||
import os
|
||||
# Third party imports
|
||||
from openai import OpenAI
|
||||
from rest_framework.response import Response
|
||||
@ -27,8 +27,8 @@ class GPTIntegrationEndpoint(BaseAPIView):
|
||||
|
||||
# Get the configuration value
|
||||
instance_configuration = InstanceConfiguration.objects.values("key", "value")
|
||||
api_key = get_configuration_value(instance_configuration, "OPENAI_API_KEY")
|
||||
gpt_engine = get_configuration_value(instance_configuration, "GPT_ENGINE")
|
||||
api_key = get_configuration_value(instance_configuration, "OPENAI_API_KEY", os.environ.get("OPENAI_API_KEY"))
|
||||
gpt_engine = get_configuration_value(instance_configuration, "GPT_ENGINE", os.environ.get("GPT_ENGINE", "gpt-3.5-turbo"))
|
||||
|
||||
# Check the keys
|
||||
if not api_key or not gpt_engine:
|
||||
@ -47,10 +47,6 @@ class GPTIntegrationEndpoint(BaseAPIView):
|
||||
|
||||
final_text = task + "\n" + prompt
|
||||
|
||||
instance_configuration = InstanceConfiguration.objects.values("key", "value")
|
||||
|
||||
gpt_engine = get_configuration_value(instance_configuration, "GPT_ENGINE")
|
||||
|
||||
client = OpenAI(
|
||||
api_key=api_key,
|
||||
)
|
||||
@ -85,14 +81,22 @@ class ReleaseNotesEndpoint(BaseAPIView):
|
||||
class UnsplashEndpoint(BaseAPIView):
|
||||
|
||||
def get(self, request):
|
||||
instance_configuration = InstanceConfiguration.objects.values("key", "value")
|
||||
unsplash_access_key = get_configuration_value(instance_configuration, "UNSPLASH_ACCESS_KEY", os.environ.get("UNSPLASH_ACCESS_KEY"))
|
||||
|
||||
# Check unsplash access key
|
||||
if not unsplash_access_key:
|
||||
return Response([], status=status.HTTP_200_OK)
|
||||
|
||||
# Query parameters
|
||||
query = request.GET.get("query", False)
|
||||
page = request.GET.get("page", 1)
|
||||
per_page = request.GET.get("per_page", 20)
|
||||
|
||||
url = (
|
||||
f"https://api.unsplash.com/search/photos/?client_id={settings.UNSPLASH_ACCESS_KEY}&query={query}&page=${page}&per_page={per_page}"
|
||||
f"https://api.unsplash.com/search/photos/?client_id={unsplash_access_key}&query={query}&page=${page}&per_page={per_page}"
|
||||
if query
|
||||
else f"https://api.unsplash.com/photos/?client_id={settings.UNSPLASH_ACCESS_KEY}&page={page}&per_page={per_page}"
|
||||
else f"https://api.unsplash.com/photos/?client_id={unsplash_access_key}&page={page}&per_page={per_page}"
|
||||
)
|
||||
|
||||
headers = {
|
||||
|
@ -596,7 +596,7 @@ class IssueActivityEndpoint(BaseAPIView):
|
||||
class IssueCommentViewSet(WebhookMixin, BaseViewSet):
|
||||
serializer_class = IssueCommentSerializer
|
||||
model = IssueComment
|
||||
webhook_event = "issue-comment"
|
||||
webhook_event = "issue_comment"
|
||||
permission_classes = [
|
||||
ProjectLitePermission,
|
||||
]
|
||||
|
@ -283,9 +283,12 @@ class ModuleViewSet(WebhookMixin, BaseViewSet):
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
|
||||
class ModuleIssueViewSet(BaseViewSet):
|
||||
class ModuleIssueViewSet(WebhookMixin, BaseViewSet):
|
||||
serializer_class = ModuleIssueSerializer
|
||||
model = ModuleIssue
|
||||
webhook_event = "module_issue"
|
||||
bulk = True
|
||||
|
||||
|
||||
filterset_fields = [
|
||||
"issue__labels__id",
|
||||
@ -461,7 +464,6 @@ class ModuleIssueViewSet(BaseViewSet):
|
||||
module_issue = ModuleIssue.objects.get(
|
||||
workspace__slug=slug, project_id=project_id, module_id=module_id, pk=pk
|
||||
)
|
||||
module_issue.delete()
|
||||
issue_activity.delay(
|
||||
type="module.activity.deleted",
|
||||
requested_data=json.dumps(
|
||||
@ -471,11 +473,12 @@ class ModuleIssueViewSet(BaseViewSet):
|
||||
}
|
||||
),
|
||||
actor_id=str(request.user.id),
|
||||
issue_id=str(pk),
|
||||
issue_id=str(module_issue.issue_id),
|
||||
project_id=str(project_id),
|
||||
current_instance=None,
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
)
|
||||
module_issue.delete()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
|
||||
|
@ -388,7 +388,7 @@ class ProjectInvitationsViewset(BaseViewSet):
|
||||
{"error": "You cannot invite a user with higher role"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
|
||||
workspace = Workspace.objects.get(slug=slug)
|
||||
|
||||
project_invitations = []
|
||||
@ -424,7 +424,7 @@ class ProjectInvitationsViewset(BaseViewSet):
|
||||
project_invitations = ProjectMemberInvite.objects.bulk_create(
|
||||
project_invitations, batch_size=10, ignore_conflicts=True
|
||||
)
|
||||
current_site = request.META.get('HTTP_ORIGIN')
|
||||
current_site = request.META.get("HTTP_ORIGIN")
|
||||
|
||||
# Send invitations
|
||||
for invitation in project_invitations:
|
||||
@ -469,6 +469,13 @@ class UserProjectInvitationsViewset(BaseViewSet):
|
||||
workspace_role = workspace_member.role
|
||||
workspace = workspace_member.workspace
|
||||
|
||||
# If the user was already part of workspace
|
||||
_ = ProjectMember.objects.filter(
|
||||
workspace__slug=slug,
|
||||
project_id__in=project_ids,
|
||||
member=request.user,
|
||||
).update(is_active=True)
|
||||
|
||||
ProjectMember.objects.bulk_create(
|
||||
[
|
||||
ProjectMember(
|
||||
@ -1040,4 +1047,4 @@ class ProjectDeployBoardViewSet(BaseViewSet):
|
||||
project_deploy_board.save()
|
||||
|
||||
serializer = ProjectDeployBoardSerializer(project_deploy_board)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
@ -48,7 +48,7 @@ class UserEndpoint(BaseViewSet):
|
||||
if WorkspaceMember.objects.filter(member=request.user, is_active=True).exists():
|
||||
return Response(
|
||||
{
|
||||
"error": "User cannot deactivate account as user is active in some workspaces"
|
||||
"error": "You cannot deactivate account as you are a member in some workspaces."
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
@ -20,9 +20,10 @@ class WebhookEndpoint(BaseAPIView):
|
||||
|
||||
def post(self, request, slug):
|
||||
workspace = Workspace.objects.get(slug=slug)
|
||||
|
||||
try:
|
||||
serializer = WebhookSerializer(data=request.data)
|
||||
serializer = WebhookSerializer(
|
||||
data=request.data, context={"request": request}
|
||||
)
|
||||
if serializer.is_valid():
|
||||
serializer.save(workspace_id=workspace.id)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
@ -79,6 +80,7 @@ class WebhookEndpoint(BaseAPIView):
|
||||
serializer = WebhookSerializer(
|
||||
webhook,
|
||||
data=request.data,
|
||||
context={request: request},
|
||||
partial=True,
|
||||
fields=(
|
||||
"id",
|
||||
|
@ -590,7 +590,7 @@ class WorkSpaceMemberViewSet(BaseViewSet):
|
||||
member_with_role=Count(
|
||||
"project_projectmember",
|
||||
filter=Q(
|
||||
project_projectmember__member_id=request.user.id,
|
||||
project_projectmember__member_id=workspace_member.id,
|
||||
project_projectmember__role=20,
|
||||
),
|
||||
),
|
||||
@ -600,7 +600,7 @@ class WorkSpaceMemberViewSet(BaseViewSet):
|
||||
):
|
||||
return Response(
|
||||
{
|
||||
"error": "User is part of some projects where they are the only admin you should leave that project first"
|
||||
"error": "User is a part of some projects where they are the only admin, they should either leave that project or promote another user to admin."
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
@ -635,7 +635,7 @@ class WorkSpaceMemberViewSet(BaseViewSet):
|
||||
):
|
||||
return Response(
|
||||
{
|
||||
"error": "You cannot leave the workspace as your the only admin of the workspace you will have to either delete the workspace or create an another admin"
|
||||
"error": "You cannot leave the workspace as you are the only admin of the workspace you will have to either delete the workspace or promote another user to admin."
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
@ -656,7 +656,7 @@ class WorkSpaceMemberViewSet(BaseViewSet):
|
||||
):
|
||||
return Response(
|
||||
{
|
||||
"error": "User is part of some projects where they are the only admin you should leave that project first"
|
||||
"error": "You are a part of some projects where you are the only admin, you should either leave the project or promote another user to admin."
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
@ -1,6 +1,7 @@
|
||||
# Python imports
|
||||
import csv
|
||||
import io
|
||||
import os
|
||||
|
||||
# Django imports
|
||||
from django.core.mail import EmailMultiAlternatives, get_connection
|
||||
@ -32,7 +33,7 @@ row_mapping = {
|
||||
"priority": "Priority",
|
||||
"estimate": "Estimate",
|
||||
"issue_cycle__cycle_id": "Cycle",
|
||||
"issue_module__module_id": "Module"
|
||||
"issue_module__module_id": "Module",
|
||||
}
|
||||
|
||||
ASSIGNEE_ID = "assignees__id"
|
||||
@ -51,17 +52,48 @@ def send_export_email(email, slug, csv_buffer):
|
||||
csv_buffer.seek(0)
|
||||
|
||||
# Configure email connection from the database
|
||||
instance_configuration = InstanceConfiguration.objects.filter(key__startswith='EMAIL_').values("key", "value")
|
||||
instance_configuration = InstanceConfiguration.objects.filter(
|
||||
key__startswith="EMAIL_"
|
||||
).values("key", "value")
|
||||
connection = get_connection(
|
||||
host=get_configuration_value(instance_configuration, "EMAIL_HOST"),
|
||||
port=int(get_configuration_value(instance_configuration, "EMAIL_PORT", "587")),
|
||||
username=get_configuration_value(instance_configuration, "EMAIL_HOST_USER"),
|
||||
password=get_configuration_value(instance_configuration, "EMAIL_HOST_PASSWORD"),
|
||||
use_tls=bool(get_configuration_value(instance_configuration, "EMAIL_USE_TLS", "1")),
|
||||
use_ssl=bool(get_configuration_value(instance_configuration, "EMAIL_USE_SSL", "0")),
|
||||
host=get_configuration_value(
|
||||
instance_configuration, "EMAIL_HOST", os.environ.get("EMAIL_HOST")
|
||||
),
|
||||
port=int(
|
||||
get_configuration_value(
|
||||
instance_configuration, "EMAIL_PORT", os.environ.get("EMAIL_PORT")
|
||||
)
|
||||
),
|
||||
username=get_configuration_value(
|
||||
instance_configuration,
|
||||
"EMAIL_HOST_USER",
|
||||
os.environ.get("EMAIL_HOST_USER"),
|
||||
),
|
||||
password=get_configuration_value(
|
||||
instance_configuration,
|
||||
"EMAIL_HOST_PASSWORD",
|
||||
os.environ.get("EMAIL_HOST_PASSWORD"),
|
||||
),
|
||||
use_tls=bool(
|
||||
get_configuration_value(
|
||||
instance_configuration,
|
||||
"EMAIL_USE_TLS",
|
||||
os.environ.get("EMAIL_USE_TLS", "1"),
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
msg = EmailMultiAlternatives(subject=subject, body=text_content, from_email=get_configuration_value(instance_configuration, "EMAIL_FROM"), to=[email], connection=connection)
|
||||
msg = EmailMultiAlternatives(
|
||||
subject=subject,
|
||||
body=text_content,
|
||||
from_email=get_configuration_value(
|
||||
instance_configuration,
|
||||
"EMAIL_FROM",
|
||||
os.environ.get("EMAIL_FROM", "Team Plane <team@mailer.plane.so>"),
|
||||
),
|
||||
to=[email],
|
||||
connection=connection,
|
||||
)
|
||||
msg.attach(f"{slug}-analytics.csv", csv_buffer.getvalue())
|
||||
msg.send(fail_silently=False)
|
||||
|
||||
|
@ -1,3 +1,6 @@
|
||||
# Python imports
|
||||
import os
|
||||
|
||||
# Django imports
|
||||
from django.core.mail import EmailMultiAlternatives, get_connection
|
||||
from django.template.loader import render_to_string
|
||||
@ -14,15 +17,13 @@ from sentry_sdk import capture_exception
|
||||
from plane.license.models import InstanceConfiguration
|
||||
from plane.license.utils.instance_value import get_configuration_value
|
||||
|
||||
|
||||
@shared_task
|
||||
def email_verification(first_name, email, token, current_site):
|
||||
|
||||
try:
|
||||
realtivelink = "/request-email-verification/" + "?token=" + str(token)
|
||||
abs_url = current_site + realtivelink
|
||||
|
||||
from_email_string = settings.EMAIL_FROM
|
||||
|
||||
subject = "Verify your Email!"
|
||||
|
||||
context = {
|
||||
@ -35,17 +36,49 @@ def email_verification(first_name, email, token, current_site):
|
||||
text_content = strip_tags(html_content)
|
||||
|
||||
# Configure email connection from the database
|
||||
instance_configuration = InstanceConfiguration.objects.filter(key__startswith='EMAIL_').values("key", "value")
|
||||
instance_configuration = InstanceConfiguration.objects.filter(
|
||||
key__startswith="EMAIL_"
|
||||
).values("key", "value")
|
||||
connection = get_connection(
|
||||
host=get_configuration_value(instance_configuration, "EMAIL_HOST"),
|
||||
port=int(get_configuration_value(instance_configuration, "EMAIL_PORT", "587")),
|
||||
username=get_configuration_value(instance_configuration, "EMAIL_HOST_USER"),
|
||||
password=get_configuration_value(instance_configuration, "EMAIL_HOST_PASSWORD"),
|
||||
use_tls=bool(get_configuration_value(instance_configuration, "EMAIL_USE_TLS", "1")),
|
||||
host=get_configuration_value(
|
||||
instance_configuration, "EMAIL_HOST", os.environ.get("EMAIL_HOST")
|
||||
),
|
||||
port=int(
|
||||
get_configuration_value(
|
||||
instance_configuration, "EMAIL_PORT", os.environ.get("EMAIL_PORT")
|
||||
)
|
||||
),
|
||||
username=get_configuration_value(
|
||||
instance_configuration,
|
||||
"EMAIL_HOST_USER",
|
||||
os.environ.get("EMAIL_HOST_USER"),
|
||||
),
|
||||
password=get_configuration_value(
|
||||
instance_configuration,
|
||||
"EMAIL_HOST_PASSWORD",
|
||||
os.environ.get("EMAIL_HOST_PASSWORD"),
|
||||
),
|
||||
use_tls=bool(
|
||||
get_configuration_value(
|
||||
instance_configuration,
|
||||
"EMAIL_USE_TLS",
|
||||
os.environ.get("EMAIL_USE_TLS", "1"),
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
# Initiate email alternatives
|
||||
msg = EmailMultiAlternatives(subject=subject, body=text_content, from_email=get_configuration_value(instance_configuration, "EMAIL_FROM"), to=[email], connection=connection)
|
||||
msg = EmailMultiAlternatives(
|
||||
subject=subject,
|
||||
body=text_content,
|
||||
from_email=get_configuration_value(
|
||||
instance_configuration,
|
||||
"EMAIL_FROM",
|
||||
os.environ.get("EMAIL_FROM", "Team Plane <team@mailer.plane.so>"),
|
||||
),
|
||||
to=[email],
|
||||
connection=connection,
|
||||
)
|
||||
msg.attach_alternative(html_content, "text/html")
|
||||
msg.send()
|
||||
return
|
||||
|
@ -1,3 +1,6 @@
|
||||
# Python import
|
||||
import os
|
||||
|
||||
# Django imports
|
||||
from django.core.mail import EmailMultiAlternatives, get_connection
|
||||
from django.template.loader import render_to_string
|
||||
@ -19,8 +22,6 @@ def forgot_password(first_name, email, uidb64, token, current_site):
|
||||
realtivelink = f"/accounts/reset-password/?uidb64={uidb64}&token={token}"
|
||||
abs_url = current_site + realtivelink
|
||||
|
||||
from_email_string = settings.EMAIL_FROM
|
||||
|
||||
subject = "Reset Your Password - Plane"
|
||||
|
||||
context = {
|
||||
@ -34,14 +35,44 @@ def forgot_password(first_name, email, uidb64, token, current_site):
|
||||
|
||||
instance_configuration = InstanceConfiguration.objects.filter(key__startswith='EMAIL_').values("key", "value")
|
||||
connection = get_connection(
|
||||
host=get_configuration_value(instance_configuration, "EMAIL_HOST"),
|
||||
port=int(get_configuration_value(instance_configuration, "EMAIL_PORT", "587")),
|
||||
username=get_configuration_value(instance_configuration, "EMAIL_HOST_USER"),
|
||||
password=get_configuration_value(instance_configuration, "EMAIL_HOST_PASSWORD"),
|
||||
use_tls=bool(get_configuration_value(instance_configuration, "EMAIL_USE_TLS", "1")),
|
||||
host=get_configuration_value(
|
||||
instance_configuration, "EMAIL_HOST", os.environ.get("EMAIL_HOST")
|
||||
),
|
||||
port=int(
|
||||
get_configuration_value(
|
||||
instance_configuration, "EMAIL_PORT", os.environ.get("EMAIL_PORT")
|
||||
)
|
||||
),
|
||||
username=get_configuration_value(
|
||||
instance_configuration,
|
||||
"EMAIL_HOST_USER",
|
||||
os.environ.get("EMAIL_HOST_USER"),
|
||||
),
|
||||
password=get_configuration_value(
|
||||
instance_configuration,
|
||||
"EMAIL_HOST_PASSWORD",
|
||||
os.environ.get("EMAIL_HOST_PASSWORD"),
|
||||
),
|
||||
use_tls=bool(
|
||||
get_configuration_value(
|
||||
instance_configuration,
|
||||
"EMAIL_USE_TLS",
|
||||
os.environ.get("EMAIL_USE_TLS", "1"),
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
msg = EmailMultiAlternatives(
|
||||
subject=subject,
|
||||
body=text_content,
|
||||
from_email=get_configuration_value(
|
||||
instance_configuration,
|
||||
"EMAIL_FROM",
|
||||
os.environ.get("EMAIL_FROM", "Team Plane <team@mailer.plane.so>"),
|
||||
),
|
||||
to=[email],
|
||||
connection=connection,
|
||||
)
|
||||
# Initiate email alternatives
|
||||
msg = EmailMultiAlternatives(subject=subject, body=text_content, from_email=get_configuration_value(instance_configuration, "EMAIL_FROM"), to=[email], connection=connection)
|
||||
msg.attach_alternative(html_content, "text/html")
|
||||
msg.send()
|
||||
return
|
||||
|
@ -1,3 +1,6 @@
|
||||
# Python imports
|
||||
import os
|
||||
|
||||
# Django imports
|
||||
from django.core.mail import EmailMultiAlternatives, get_connection
|
||||
from django.template.loader import render_to_string
|
||||
@ -12,6 +15,7 @@ from sentry_sdk import capture_exception
|
||||
from plane.license.models import InstanceConfiguration
|
||||
from plane.license.utils.instance_value import get_configuration_value
|
||||
|
||||
|
||||
@shared_task
|
||||
def magic_link(email, key, token, current_site):
|
||||
try:
|
||||
@ -26,17 +30,48 @@ def magic_link(email, key, token, current_site):
|
||||
|
||||
text_content = strip_tags(html_content)
|
||||
|
||||
instance_configuration = InstanceConfiguration.objects.filter(key__startswith='EMAIL_').values("key", "value")
|
||||
instance_configuration = InstanceConfiguration.objects.filter(
|
||||
key__startswith="EMAIL_"
|
||||
).values("key", "value")
|
||||
connection = get_connection(
|
||||
host=get_configuration_value(instance_configuration, "EMAIL_HOST"),
|
||||
port=int(get_configuration_value(instance_configuration, "EMAIL_PORT", "587")),
|
||||
username=get_configuration_value(instance_configuration, "EMAIL_HOST_USER"),
|
||||
password=get_configuration_value(instance_configuration, "EMAIL_HOST_PASSWORD"),
|
||||
use_tls=bool(get_configuration_value(instance_configuration, "EMAIL_USE_TLS", "1")),
|
||||
host=get_configuration_value(
|
||||
instance_configuration, "EMAIL_HOST", os.environ.get("EMAIL_HOST")
|
||||
),
|
||||
port=int(
|
||||
get_configuration_value(
|
||||
instance_configuration, "EMAIL_PORT", os.environ.get("EMAIL_PORT")
|
||||
)
|
||||
),
|
||||
username=get_configuration_value(
|
||||
instance_configuration,
|
||||
"EMAIL_HOST_USER",
|
||||
os.environ.get("EMAIL_HOST_USER"),
|
||||
),
|
||||
password=get_configuration_value(
|
||||
instance_configuration,
|
||||
"EMAIL_HOST_PASSWORD",
|
||||
os.environ.get("EMAIL_HOST_PASSWORD"),
|
||||
),
|
||||
use_tls=bool(
|
||||
get_configuration_value(
|
||||
instance_configuration,
|
||||
"EMAIL_USE_TLS",
|
||||
os.environ.get("EMAIL_USE_TLS", "1"),
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
# Initiate email alternatives
|
||||
msg = EmailMultiAlternatives(subject=subject, body=text_content, from_email=get_configuration_value(instance_configuration, "EMAIL_FROM"), to=[email], connection=connection)
|
||||
msg = EmailMultiAlternatives(
|
||||
subject=subject,
|
||||
body=text_content,
|
||||
from_email=get_configuration_value(
|
||||
instance_configuration,
|
||||
"EMAIL_FROM",
|
||||
os.environ.get("EMAIL_FROM", "Team Plane <team@mailer.plane.so>"),
|
||||
),
|
||||
to=[email],
|
||||
connection=connection,
|
||||
)
|
||||
msg.attach_alternative(html_content, "text/html")
|
||||
msg.send()
|
||||
return
|
||||
|
@ -190,6 +190,7 @@ def notifications(type, issue_id, project_id, actor_id, subscriber, issue_activi
|
||||
issue_activities_created) if issue_activities_created is not None else None
|
||||
)
|
||||
if type not in [
|
||||
"issue.activity.deleted",
|
||||
"cycle.activity.created",
|
||||
"cycle.activity.deleted",
|
||||
"module.activity.created",
|
||||
|
@ -1,3 +1,6 @@
|
||||
# Python import
|
||||
import os
|
||||
|
||||
# Django imports
|
||||
from django.core.mail import EmailMultiAlternatives, get_connection
|
||||
from django.template.loader import render_to_string
|
||||
@ -25,8 +28,6 @@ def project_invitation(email, project_id, token, current_site, invitor):
|
||||
relativelink = f"/project-invitations/?invitation_id={project_member_invite.id}&email={email}&slug={project.workspace.slug}&project_id={str(project_id)}"
|
||||
abs_url = current_site + relativelink
|
||||
|
||||
from_email_string = settings.EMAIL_FROM
|
||||
|
||||
subject = f"{user.first_name or user.display_name or user.email} invited you to join {project.name} on Plane"
|
||||
|
||||
context = {
|
||||
@ -48,14 +49,45 @@ def project_invitation(email, project_id, token, current_site, invitor):
|
||||
# Configure email connection from the database
|
||||
instance_configuration = InstanceConfiguration.objects.filter(key__startswith='EMAIL_').values("key", "value")
|
||||
connection = get_connection(
|
||||
host=get_configuration_value(instance_configuration, "EMAIL_HOST"),
|
||||
port=int(get_configuration_value(instance_configuration, "EMAIL_PORT", "587")),
|
||||
username=get_configuration_value(instance_configuration, "EMAIL_HOST_USER"),
|
||||
password=get_configuration_value(instance_configuration, "EMAIL_HOST_PASSWORD"),
|
||||
use_tls=bool(get_configuration_value(instance_configuration, "EMAIL_USE_TLS", "1")),
|
||||
host=get_configuration_value(
|
||||
instance_configuration, "EMAIL_HOST", os.environ.get("EMAIL_HOST")
|
||||
),
|
||||
port=int(
|
||||
get_configuration_value(
|
||||
instance_configuration, "EMAIL_PORT", os.environ.get("EMAIL_PORT")
|
||||
)
|
||||
),
|
||||
username=get_configuration_value(
|
||||
instance_configuration,
|
||||
"EMAIL_HOST_USER",
|
||||
os.environ.get("EMAIL_HOST_USER"),
|
||||
),
|
||||
password=get_configuration_value(
|
||||
instance_configuration,
|
||||
"EMAIL_HOST_PASSWORD",
|
||||
os.environ.get("EMAIL_HOST_PASSWORD"),
|
||||
),
|
||||
use_tls=bool(
|
||||
get_configuration_value(
|
||||
instance_configuration,
|
||||
"EMAIL_USE_TLS",
|
||||
os.environ.get("EMAIL_USE_TLS", "1"),
|
||||
)
|
||||
),
|
||||
)
|
||||
# Initiate email alternatives
|
||||
msg = EmailMultiAlternatives(subject=subject, body=text_content, from_email=get_configuration_value(instance_configuration, "EMAIL_FROM"), to=[email], connection=connection)
|
||||
|
||||
msg = EmailMultiAlternatives(
|
||||
subject=subject,
|
||||
body=text_content,
|
||||
from_email=get_configuration_value(
|
||||
instance_configuration,
|
||||
"EMAIL_FROM",
|
||||
os.environ.get("EMAIL_FROM", "Team Plane <team@mailer.plane.so>"),
|
||||
),
|
||||
to=[email],
|
||||
connection=connection,
|
||||
)
|
||||
|
||||
msg.attach_alternative(html_content, "text/html")
|
||||
msg.send()
|
||||
return
|
||||
|
@ -2,15 +2,67 @@ import requests
|
||||
import uuid
|
||||
import hashlib
|
||||
import json
|
||||
import hmac
|
||||
|
||||
# Django imports
|
||||
from django.conf import settings
|
||||
from django.core.serializers.json import DjangoJSONEncoder
|
||||
|
||||
# Third party imports
|
||||
from celery import shared_task
|
||||
from sentry_sdk import capture_exception
|
||||
|
||||
from plane.db.models import Webhook, WebhookLog
|
||||
from plane.db.models import (
|
||||
Webhook,
|
||||
WebhookLog,
|
||||
Project,
|
||||
Issue,
|
||||
Cycle,
|
||||
Module,
|
||||
ModuleIssue,
|
||||
CycleIssue,
|
||||
IssueComment,
|
||||
)
|
||||
from plane.api.serializers import (
|
||||
ProjectSerializer,
|
||||
IssueSerializer,
|
||||
CycleSerializer,
|
||||
ModuleSerializer,
|
||||
CycleIssueSerializer,
|
||||
ModuleIssueSerializer,
|
||||
IssueCommentSerializer,
|
||||
IssueExpandSerializer,
|
||||
)
|
||||
|
||||
SERIALIZER_MAPPER = {
|
||||
"project": ProjectSerializer,
|
||||
"issue": IssueExpandSerializer,
|
||||
"cycle": CycleSerializer,
|
||||
"module": ModuleSerializer,
|
||||
"cycle_issue": CycleIssueSerializer,
|
||||
"module_issue": ModuleIssueSerializer,
|
||||
"issue_comment": IssueCommentSerializer,
|
||||
}
|
||||
|
||||
MODEL_MAPPER = {
|
||||
"project": Project,
|
||||
"issue": Issue,
|
||||
"cycle": Cycle,
|
||||
"module": Module,
|
||||
"cycle_issue": CycleIssue,
|
||||
"module_issue": ModuleIssue,
|
||||
"issue_comment": IssueComment,
|
||||
}
|
||||
|
||||
|
||||
def get_model_data(event, event_id, many=False):
|
||||
model = MODEL_MAPPER.get(event)
|
||||
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, many=many).data
|
||||
|
||||
|
||||
@shared_task(
|
||||
@ -31,19 +83,24 @@ def webhook_task(self, webhook, slug, event, event_data, action):
|
||||
"X-Plane-Event": event,
|
||||
}
|
||||
|
||||
# Your secret key
|
||||
# # Your secret key
|
||||
event_data = (
|
||||
json.loads(json.dumps(event_data, cls=DjangoJSONEncoder))
|
||||
if event_data is not None
|
||||
else None
|
||||
)
|
||||
|
||||
# Use HMAC for generating signature
|
||||
if webhook.secret_key:
|
||||
# Concatenate the data and the secret key
|
||||
message = event_data + webhook.secret_key
|
||||
|
||||
# Create a SHA-256 hash of the message
|
||||
sha256 = hashlib.sha256()
|
||||
sha256.update(message.encode("utf-8"))
|
||||
signature = sha256.hexdigest()
|
||||
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,
|
||||
)
|
||||
signature = hmac_signature.hexdigest()
|
||||
headers["X-Plane-Signature"] = signature
|
||||
|
||||
event_data = json.loads(event_data) if event_data is not None else None
|
||||
|
||||
action = {
|
||||
"POST": "create",
|
||||
"PATCH": "update",
|
||||
@ -96,10 +153,6 @@ def webhook_task(self, webhook, slug, event, event_data, action):
|
||||
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)
|
||||
return
|
||||
raise requests.RequestException()
|
||||
|
||||
except Exception as e:
|
||||
@ -110,7 +163,7 @@ def webhook_task(self, webhook, slug, event, event_data, action):
|
||||
|
||||
|
||||
@shared_task()
|
||||
def send_webhook(event, event_data, action, slug):
|
||||
def send_webhook(event, payload, kw, action, slug, bulk):
|
||||
try:
|
||||
webhooks = Webhook.objects.filter(workspace__slug=slug, is_active=True)
|
||||
|
||||
@ -120,17 +173,48 @@ def send_webhook(event, event_data, action, slug):
|
||||
if event == "issue":
|
||||
webhooks = webhooks.filter(issue=True)
|
||||
|
||||
if event == "module":
|
||||
if event == "module" or event == "module_issue":
|
||||
webhooks = webhooks.filter(module=True)
|
||||
|
||||
if event == "cycle":
|
||||
if event == "cycle" or event == "cycle_issue":
|
||||
webhooks = webhooks.filter(cycle=True)
|
||||
|
||||
if event == "issue-comment":
|
||||
if event == "issue_comment":
|
||||
webhooks = webhooks.filter(issue_comment=True)
|
||||
|
||||
for webhook in webhooks:
|
||||
webhook_task.delay(webhook.id, slug, event, event_data, action)
|
||||
if webhooks:
|
||||
if action in ["POST", "PATCH"]:
|
||||
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_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,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
if settings.DEBUG:
|
||||
|
@ -1,3 +1,6 @@
|
||||
# Python imports
|
||||
import os
|
||||
|
||||
# Django imports
|
||||
from django.core.mail import EmailMultiAlternatives, get_connection
|
||||
from django.template.loader import render_to_string
|
||||
@ -32,9 +35,6 @@ def workspace_invitation(email, workspace_id, token, current_site, invitor):
|
||||
# The complete url including the domain
|
||||
abs_url = current_site + relative_link
|
||||
|
||||
# The email from
|
||||
from_email_string = settings.EMAIL_FROM
|
||||
|
||||
# Subject of the email
|
||||
subject = f"{user.first_name or user.display_name or user.email} invited you to join {workspace.name} on Plane"
|
||||
|
||||
@ -58,23 +58,41 @@ def workspace_invitation(email, workspace_id, token, current_site, invitor):
|
||||
key__startswith="EMAIL_"
|
||||
).values("key", "value")
|
||||
connection = get_connection(
|
||||
host=get_configuration_value(instance_configuration, "EMAIL_HOST"),
|
||||
port=int(
|
||||
get_configuration_value(instance_configuration, "EMAIL_PORT", "587")
|
||||
host=get_configuration_value(
|
||||
instance_configuration, "EMAIL_HOST", os.environ.get("EMAIL_HOST")
|
||||
),
|
||||
port=int(
|
||||
get_configuration_value(
|
||||
instance_configuration, "EMAIL_PORT", os.environ.get("EMAIL_PORT")
|
||||
)
|
||||
),
|
||||
username=get_configuration_value(
|
||||
instance_configuration,
|
||||
"EMAIL_HOST_USER",
|
||||
os.environ.get("EMAIL_HOST_USER"),
|
||||
),
|
||||
username=get_configuration_value(instance_configuration, "EMAIL_HOST_USER"),
|
||||
password=get_configuration_value(
|
||||
instance_configuration, "EMAIL_HOST_PASSWORD"
|
||||
instance_configuration,
|
||||
"EMAIL_HOST_PASSWORD",
|
||||
os.environ.get("EMAIL_HOST_PASSWORD"),
|
||||
),
|
||||
use_tls=bool(
|
||||
get_configuration_value(instance_configuration, "EMAIL_USE_TLS", "1")
|
||||
get_configuration_value(
|
||||
instance_configuration,
|
||||
"EMAIL_USE_TLS",
|
||||
os.environ.get("EMAIL_USE_TLS", "1"),
|
||||
)
|
||||
),
|
||||
)
|
||||
# Initiate email alternatives
|
||||
|
||||
msg = EmailMultiAlternatives(
|
||||
subject=subject,
|
||||
body=text_content,
|
||||
from_email=get_configuration_value(instance_configuration, "EMAIL_FROM"),
|
||||
from_email=get_configuration_value(
|
||||
instance_configuration,
|
||||
"EMAIL_FROM",
|
||||
os.environ.get("EMAIL_FROM", "Team Plane <team@mailer.plane.so>"),
|
||||
),
|
||||
to=[email],
|
||||
connection=connection,
|
||||
)
|
||||
|
19
apiserver/plane/db/migrations/0052_alter_workspace_slug.py
Normal file
19
apiserver/plane/db/migrations/0052_alter_workspace_slug.py
Normal file
@ -0,0 +1,19 @@
|
||||
# Generated by Django 4.2.5 on 2023-11-23 14:57
|
||||
|
||||
from django.db import migrations, models
|
||||
import plane.db.models.workspace
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('db', '0051_fileasset_is_deleted'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='workspace',
|
||||
name='slug',
|
||||
field=models.SlugField(max_length=48, unique=True, validators=[plane.db.models.workspace.slug_validator]),
|
||||
),
|
||||
]
|
@ -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
|
||||
|
@ -16,7 +16,6 @@ def generate_token():
|
||||
|
||||
def validate_schema(value):
|
||||
parsed_url = urlparse(value)
|
||||
print(parsed_url)
|
||||
if parsed_url.scheme not in ["http", "https"]:
|
||||
raise ValidationError("Invalid schema. Only HTTP and HTTPS are allowed.")
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
# Django imports
|
||||
from django.db import models
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ValidationError
|
||||
|
||||
# Module imports
|
||||
from . import BaseModel
|
||||
@ -50,7 +51,7 @@ def get_default_props():
|
||||
"state": True,
|
||||
"sub_issue_count": True,
|
||||
"updated_on": True,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@ -63,6 +64,23 @@ def get_issue_props():
|
||||
}
|
||||
|
||||
|
||||
def slug_validator(value):
|
||||
if value in [
|
||||
"404",
|
||||
"accounts",
|
||||
"api",
|
||||
"create-workspace",
|
||||
"god-mode",
|
||||
"installations",
|
||||
"invitations",
|
||||
"onboarding",
|
||||
"profile",
|
||||
"spaces",
|
||||
"workspace-invitations",
|
||||
]:
|
||||
raise ValidationError("Slug is not valid")
|
||||
|
||||
|
||||
class Workspace(BaseModel):
|
||||
name = models.CharField(max_length=80, verbose_name="Workspace Name")
|
||||
logo = models.URLField(verbose_name="Logo", blank=True, null=True)
|
||||
@ -71,7 +89,7 @@ class Workspace(BaseModel):
|
||||
on_delete=models.CASCADE,
|
||||
related_name="owner_workspace",
|
||||
)
|
||||
slug = models.SlugField(max_length=48, db_index=True, unique=True)
|
||||
slug = models.SlugField(max_length=48, db_index=True, unique=True, validators=[slug_validator,])
|
||||
organization_size = models.CharField(max_length=20, blank=True, null=True)
|
||||
|
||||
def __str__(self):
|
||||
|
@ -30,9 +30,11 @@ class Command(BaseCommand):
|
||||
"EMAIL_USE_TLS": os.environ.get("EMAIL_USE_TLS", "1"),
|
||||
"EMAIL_USE_SSL": os.environ.get("EMAIL_USE_SSL", "0"),
|
||||
# Open AI Settings
|
||||
"OPENAI_API_BASE": os.environ.get("", "https://api.openai.com/v1"),
|
||||
"OPENAI_API_KEY": os.environ.get("OPENAI_API_KEY", "sk-"),
|
||||
"OPENAI_API_BASE": os.environ.get("OPENAI_API_BASE", "https://api.openai.com/v1"),
|
||||
"OPENAI_API_KEY": os.environ.get("OPENAI_API_KEY", ""),
|
||||
"GPT_ENGINE": os.environ.get("GPT_ENGINE", "gpt-3.5-turbo"),
|
||||
# Unsplash Access Key
|
||||
"UNSPLASH_ACCESS_KEY": os.environ.get("UNSPLASH_ACESS_KEY", "")
|
||||
}
|
||||
|
||||
for key, value in config_keys.items():
|
||||
|
Loading…
Reference in New Issue
Block a user