From 0de079fc0ae4e1d2cb24de30b23fd1fb63f9a98c Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Tue, 7 Feb 2023 15:10:47 +0530 Subject: [PATCH 01/36] feat: initiate integrations --- apiserver/plane/db/models/integration/base.py | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 apiserver/plane/db/models/integration/base.py diff --git a/apiserver/plane/db/models/integration/base.py b/apiserver/plane/db/models/integration/base.py new file mode 100644 index 00000000000..d3997a16ce9 --- /dev/null +++ b/apiserver/plane/db/models/integration/base.py @@ -0,0 +1,53 @@ +# Django imports +from django.db import models + +# Module imports +from plane.db.models import BaseModel +from plane.db.mixins import AuditModel + + +class Integration(AuditModel): + provider = models.CharField(max_length=400) + network = models.PositiveIntegerField( + default=1, choices=((1, "Private"), (2, "Public")) + ) + description = models.JSONField(default=dict) + author = models.CharField(max_length=400, blank=True) + webhook_url = models.TextField(blank=True) + webhook_secret = models.TextField(blank=True) + redirect_url = models.TextField(blank=True) + metadata = models.JSONField(default=dict) + + def __str__(self): + """Return provider of the integration""" + return f"{self.provider}" + + class Meta: + verbose_name = "Integration" + verbose_name_plural = "Integrations" + db_table = "integrations" + ordering = ("-created_at",) + + +class WorkspaceIntegration(BaseModel): + workspace = models.ForeignKey( + "db.Workspace", related_name="workspace_integrations", on_delete=models.CASCADE + ) + # Bot user + user = models.ForeignKey( + "db.User", related_name="integrations", on_delete=models.CASCADE + ) + integration = models.ForeignKey( + "db.Integration", related_name="integrated_workspaces", on_delete=models.CASCADE + ) + metadata = models.JSONField(default=dict) + + def __str__(self): + """Return name of the project""" + return f"{self.workspace.name} <{self.integration.provider}>" + + class Meta: + verbose_name = "Workspace Integration" + verbose_name_plural = "Workspace Integrations" + db_table = "workspace_integrations" + ordering = ("-created_at",) From e4a5a19fde43c141c7c379daae629dfeecad2c4f Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Tue, 7 Feb 2023 15:11:51 +0530 Subject: [PATCH 02/36] feat: initiate github integration create models for the same --- apiserver/plane/db/models/__init__.py | 20 ++++++- .../plane/db/models/integration/__init__.py | 2 + apiserver/plane/db/models/integration/base.py | 2 +- .../plane/db/models/integration/github.py | 56 +++++++++++++++++++ 4 files changed, 76 insertions(+), 4 deletions(-) create mode 100644 apiserver/plane/db/models/integration/__init__.py create mode 100644 apiserver/plane/db/models/integration/github.py diff --git a/apiserver/plane/db/models/__init__.py b/apiserver/plane/db/models/__init__.py index ef7ad5b8d49..901035d679f 100644 --- a/apiserver/plane/db/models/__init__.py +++ b/apiserver/plane/db/models/__init__.py @@ -10,7 +10,13 @@ TeamMember, ) -from .project import Project, ProjectMember, ProjectBaseModel, ProjectMemberInvite, ProjectIdentifier +from .project import ( + Project, + ProjectMember, + ProjectBaseModel, + ProjectMemberInvite, + ProjectIdentifier, +) from .issue import ( Issue, @@ -37,6 +43,14 @@ from .view import View -from .module import Module, ModuleMember, ModuleIssue, ModuleLink +from .module import Module, ModuleMember, ModuleIssue, ModuleLink + +from .api_token import APIToken -from .api_token import APIToken \ No newline at end of file +from .integration import ( + WorkspaceIntegration, + Integration, + Repository, + GithubRepositorySync, + GithubIssueSync, +) diff --git a/apiserver/plane/db/models/integration/__init__.py b/apiserver/plane/db/models/integration/__init__.py new file mode 100644 index 00000000000..3db60b90117 --- /dev/null +++ b/apiserver/plane/db/models/integration/__init__.py @@ -0,0 +1,2 @@ +from .base import Integration, WorkspaceIntegration +from .github import Repository, GithubRepositorySync, GithubIssueSync diff --git a/apiserver/plane/db/models/integration/base.py b/apiserver/plane/db/models/integration/base.py index d3997a16ce9..dbcc9a6c722 100644 --- a/apiserver/plane/db/models/integration/base.py +++ b/apiserver/plane/db/models/integration/base.py @@ -43,7 +43,7 @@ class WorkspaceIntegration(BaseModel): metadata = models.JSONField(default=dict) def __str__(self): - """Return name of the project""" + """Return name of the integration and workspace""" return f"{self.workspace.name} <{self.integration.provider}>" class Meta: diff --git a/apiserver/plane/db/models/integration/github.py b/apiserver/plane/db/models/integration/github.py new file mode 100644 index 00000000000..a4d47a3f97e --- /dev/null +++ b/apiserver/plane/db/models/integration/github.py @@ -0,0 +1,56 @@ +# Django imports +from django.db import models + +# Module imports +from plane.db.models import ProjectBaseModel + + +class Repository(ProjectBaseModel): + name = models.CharField(max_length=500) + url = models.URLField(null=True) + config = models.JSONField(default=dict) + + def __str__(self): + """Return the repo name""" + return f"{self.name}" + + class Meta: + verbose_name = "Repository" + verbose_name_plural = "Repositories" + db_table = "repositories" + ordering = ("-created_at",) + + +class GithubRepositorySync(ProjectBaseModel): + repository_id = models.BigIntegerField() + credentials = models.JSONField(default=dict) + + def __str__(self): + """Return the repo sync""" + return f"{self.repository.name} <{self.project.name}>" + + class Meta: + verbose_name = "Github Repository Sync" + verbose_name_plural = "Github Repository Syncs" + db_table = "github_repository_syncs" + ordering = ("-created_at",) + + +class GithubIssueSync(ProjectBaseModel): + github_issue_id = models.BigIntegerField() + issue = models.ForeignKey( + "db.Issue", related_name="github_syncs", on_delete=models.CASCADE + ) + repository = models.ForeignKey( + "db.Repository", related_name="issue_syncs", on_delete=models.CASCADE + ) + + def __str__(self): + """Return the github issue sync""" + return f"{self.repository.name}-{self.project.name}-{self.issue.name}" + + class Meta: + verbose_name = "Github Issue Sync" + verbose_name_plural = "Github Issue Syncs" + db_table = "github_issue_syncs" + ordering = ("-created_at",) From 8726aeb252a0d2c73987b50bd5a82963e0ea880f Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Wed, 8 Feb 2023 00:20:42 +0530 Subject: [PATCH 03/36] feat: github integration views --- apiserver/plane/api/serializers/__init__.py | 10 +- .../api/serializers/integration/__init__.py | 6 + .../plane/api/serializers/integration/base.py | 18 +++ .../api/serializers/integration/github.py | 21 ++++ apiserver/plane/api/urls.py | 54 +++++++- apiserver/plane/api/views/__init__.py | 10 +- .../plane/api/views/integration/__init__.py | 6 + apiserver/plane/api/views/integration/base.py | 116 ++++++++++++++++++ .../plane/api/views/integration/github.py | 35 ++++++ apiserver/plane/db/models/__init__.py | 2 +- .../plane/db/models/integration/__init__.py | 2 +- apiserver/plane/db/models/integration/base.py | 2 + .../plane/db/models/integration/github.py | 6 +- 13 files changed, 280 insertions(+), 8 deletions(-) create mode 100644 apiserver/plane/api/serializers/integration/__init__.py create mode 100644 apiserver/plane/api/serializers/integration/base.py create mode 100644 apiserver/plane/api/serializers/integration/github.py create mode 100644 apiserver/plane/api/views/integration/__init__.py create mode 100644 apiserver/plane/api/views/integration/base.py create mode 100644 apiserver/plane/api/views/integration/github.py diff --git a/apiserver/plane/api/serializers/__init__.py b/apiserver/plane/api/serializers/__init__.py index 8d43d90ffed..2830536f5df 100644 --- a/apiserver/plane/api/serializers/__init__.py +++ b/apiserver/plane/api/serializers/__init__.py @@ -40,4 +40,12 @@ from .module import ModuleWriteSerializer, ModuleSerializer, ModuleIssueSerializer -from .api_token import APITokenSerializer \ No newline at end of file +from .api_token import APITokenSerializer + +from .integration import ( + IntegrationSerializer, + WorkspaceIntegrationSerializer, + GithubIssueSyncSerializer, + GithubRepositorySerializer, + GithubRepositorySyncSerializer, +) diff --git a/apiserver/plane/api/serializers/integration/__init__.py b/apiserver/plane/api/serializers/integration/__init__.py new file mode 100644 index 00000000000..73173a30c78 --- /dev/null +++ b/apiserver/plane/api/serializers/integration/__init__.py @@ -0,0 +1,6 @@ +from .base import IntegrationSerializer, WorkspaceIntegrationSerializer +from .github import ( + GithubRepositorySerializer, + GithubRepositorySyncSerializer, + GithubIssueSyncSerializer, +) diff --git a/apiserver/plane/api/serializers/integration/base.py b/apiserver/plane/api/serializers/integration/base.py new file mode 100644 index 00000000000..f2ff83482d4 --- /dev/null +++ b/apiserver/plane/api/serializers/integration/base.py @@ -0,0 +1,18 @@ +# Module imports +from plane.api.serializers import BaseSerializer +from plane.db.models import Integration, WorkspaceIntegration + + +class IntegrationSerializer(BaseSerializer): + class Meta: + model = Integration + fields = "__all__" + read_only_fields = [ + "verified", + ] + + +class WorkspaceIntegrationSerializer(BaseSerializer): + class Meta: + model = WorkspaceIntegration + fields = "__all__" diff --git a/apiserver/plane/api/serializers/integration/github.py b/apiserver/plane/api/serializers/integration/github.py new file mode 100644 index 00000000000..6279b0624b1 --- /dev/null +++ b/apiserver/plane/api/serializers/integration/github.py @@ -0,0 +1,21 @@ +# Module imports +from plane.api.serializers import BaseSerializer +from plane.db.models import GithubIssueSync, GithubRepository, GithubRepositorySync + + +class GithubRepositorySerializer(BaseSerializer): + class Meta: + model = GithubRepository + fields = "__all__" + + +class GithubRepositorySyncSerializer(BaseSerializer): + class Meta: + model = GithubRepositorySync + fields = "__all__" + + +class GithubIssueSyncSerializer(BaseSerializer): + class Meta: + model = GithubIssueSync + fields = "__all__" diff --git a/apiserver/plane/api/urls.py b/apiserver/plane/api/urls.py index 98c2e87d286..57c91921209 100644 --- a/apiserver/plane/api/urls.py +++ b/apiserver/plane/api/urls.py @@ -87,6 +87,13 @@ # Api Tokens ApiTokenEndpoint, ## End Api Tokens + # Integrations + IntegrationViewSet, + WorkspaceIntegrationViewSet, + GithubRepoViewSet, + GithubRepositorySyncViewSet, + GithubIssueSyncViewSet, + ## End Integrations ) @@ -683,7 +690,50 @@ ), ## End Modules # API Tokens - path("api-tokens/", ApiTokenEndpoint.as_view(), name="api-token"), - path("api-tokens//", ApiTokenEndpoint.as_view(), name="api-token"), + path("api-tokens/", ApiTokenEndpoint.as_view(), name="api-tokens"), + path("api-tokens//", ApiTokenEndpoint.as_view(), name="api-tokens"), ## End API Tokens + # Integrations + path( + "integrations/", + IntegrationViewSet.as_view( + { + "get": "list", + "post": "create", + } + ), + name="integrations", + ), + path( + "integrations//", + IntegrationViewSet.as_view( + { + "get": "retrieve", + "patch": "partial_update", + "delete": "destroy", + } + ), + name="integrations", + ), + path( + "workspaces//workspace-integrations/", + WorkspaceIntegrationViewSet.as_view( + { + "get": "list", + "post": "create", + } + ), + name="workspace-integrations", + ), + path( + "workspaces//workspace-integrations//", + WorkspaceIntegrationViewSet.as_view( + { + "get": "retrieve", + "delete": "destroy", + } + ), + name="workspace-integrations", + ), + ## End Integrations ] diff --git a/apiserver/plane/api/views/__init__.py b/apiserver/plane/api/views/__init__.py index 1212e0dca0d..41145900b07 100644 --- a/apiserver/plane/api/views/__init__.py +++ b/apiserver/plane/api/views/__init__.py @@ -73,4 +73,12 @@ from .module import ModuleViewSet, ModuleIssueViewSet -from .api_token import ApiTokenEndpoint \ No newline at end of file +from .api_token import ApiTokenEndpoint + +from .integration import ( + WorkspaceIntegrationViewSet, + IntegrationViewSet, + GithubIssueSyncViewSet, + GithubRepositorySyncViewSet, + GithubRepoViewSet, +) diff --git a/apiserver/plane/api/views/integration/__init__.py b/apiserver/plane/api/views/integration/__init__.py new file mode 100644 index 00000000000..fd6df0e1368 --- /dev/null +++ b/apiserver/plane/api/views/integration/__init__.py @@ -0,0 +1,6 @@ +from .base import IntegrationViewSet, WorkspaceIntegrationViewSet +from .github import ( + GithubRepoViewSet, + GithubRepositorySyncViewSet, + GithubIssueSyncViewSet, +) diff --git a/apiserver/plane/api/views/integration/base.py b/apiserver/plane/api/views/integration/base.py new file mode 100644 index 00000000000..8e7a3808788 --- /dev/null +++ b/apiserver/plane/api/views/integration/base.py @@ -0,0 +1,116 @@ +# Python improts +import uuid + +# Django imports +from django.db import IntegrityError + +# Third party imports +from rest_framework.response import Response +from rest_framework import status +from sentry_sdk import capture_exception + +# Module imports +from plane.api.views import BaseViewSet +from plane.db.models import Integration, WorkspaceIntegration, Workspace, User +from plane.api.serializers import IntegrationSerializer, WorkspaceIntegrationSerializer + + +class IntegrationViewSet(BaseViewSet): + serializer_class = IntegrationSerializer + model = Integration + + def create(self, request): + try: + serializer = IntegrationSerializer(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) + except Exception as e: + capture_exception(e) + return Response( + {"error": "Something went wrong please try again later"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + def partial_update(self, request, pk): + try: + integration = Integration.objects.get(pk=pk) + if integration.verified: + return Response( + {"error": "Verified integrations cannot be updated"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + serializer = IntegrationSerializer( + integration, data=request.data, partial=True + ) + + 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) + + except Integration.DoesNotExist: + return Response( + {"error": "Integration Does not exist"}, + status=status.HTTP_404_NOT_FOUND, + ) + except Exception as e: + capture_exception(e) + return Response( + {"error": "Something went wrong please try again later"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + +class WorkspaceIntegrationViewSet(BaseViewSet): + serializer_class = WorkspaceIntegrationSerializer + model = WorkspaceIntegration + + def create(self, request, slug): + try: + integration = request.data.get("integration", False) + + if not integration: + return Response( + {"error": "Integration is required"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + workspace = Workspace.objects.get(slug=slug) + integration = Integration.objects.get(pk=integration) + + # Create a bot user + bot_user = User.objects.create( + email=f"{str(integration.id)-str(workspace.id)}@plane.so", + username=uuid.uuid4().hex, + ) + + workspace_integration = WorkspaceIntegration.objects.create( + workspace=workspace, + integration=integration, + user=bot_user, + ) + + return Response( + WorkspaceIntegrationSerializer(workspace_integration).data, + status=status.HTTP_201_CREATED, + ) + except IntegrityError as e: + if "already exists" in str(e): + return Response( + {"error": "Integration is already active in the workspace"}, + status=status.HTTP_410_GONE, + ) + except (Workspace.DoesNotExist, Integration.DoesNotExist) as e: + return Response( + {"error": "Workspace or Integration not found"}, + status=status.HTTP_400_BAD_REQUEST, + ) + except Exception as e: + capture_exception(e) + return Response( + {"error": "Something went wrong please try again later"}, + status=status.HTTP_400_BAD_REQUEST, + ) diff --git a/apiserver/plane/api/views/integration/github.py b/apiserver/plane/api/views/integration/github.py new file mode 100644 index 00000000000..aa8867053f1 --- /dev/null +++ b/apiserver/plane/api/views/integration/github.py @@ -0,0 +1,35 @@ +# Third party imports +from rest_framework import status + +# Module imports +from plane.api.views import BaseViewSet +from plane.db.models import GithubIssueSync, GithubRepositorySync, GithubRepository +from plane.api.serializers import ( + GithubIssueSyncSerializer, + GithubRepositorySyncSerializer, + GithubRepositorySerializer, +) + + +class GithubRepoViewSet(BaseViewSet): + serializer_class = GithubRepositorySerializer + model = GithubRepository + + def perform_create(self, serializer): + serializer.save(project_id=self.kwargs.get("project_id")) + + +class GithubRepositorySyncViewSet(BaseViewSet): + serializer_class = GithubRepositorySyncSerializer + model = GithubRepositorySync + + def perform_create(self, serializer): + serializer.save(project_id=self.kwargs.get("project_id")) + + +class GithubIssueSyncViewSet(BaseViewSet): + serializer_class = GithubIssueSyncSerializer + model = GithubIssueSync + + def perform_create(self, serializer): + serializer.save(project_id=self.kwargs.get("project_id")) diff --git a/apiserver/plane/db/models/__init__.py b/apiserver/plane/db/models/__init__.py index 901035d679f..6c9ccf58125 100644 --- a/apiserver/plane/db/models/__init__.py +++ b/apiserver/plane/db/models/__init__.py @@ -50,7 +50,7 @@ from .integration import ( WorkspaceIntegration, Integration, - Repository, + GithubRepository, GithubRepositorySync, GithubIssueSync, ) diff --git a/apiserver/plane/db/models/integration/__init__.py b/apiserver/plane/db/models/integration/__init__.py index 3db60b90117..74265a6f469 100644 --- a/apiserver/plane/db/models/integration/__init__.py +++ b/apiserver/plane/db/models/integration/__init__.py @@ -1,2 +1,2 @@ from .base import Integration, WorkspaceIntegration -from .github import Repository, GithubRepositorySync, GithubIssueSync +from .github import GithubRepository, GithubRepositorySync, GithubIssueSync diff --git a/apiserver/plane/db/models/integration/base.py b/apiserver/plane/db/models/integration/base.py index dbcc9a6c722..e2d5486fa43 100644 --- a/apiserver/plane/db/models/integration/base.py +++ b/apiserver/plane/db/models/integration/base.py @@ -17,6 +17,7 @@ class Integration(AuditModel): webhook_secret = models.TextField(blank=True) redirect_url = models.TextField(blank=True) metadata = models.JSONField(default=dict) + verified = models.BooleanField(default=False) def __str__(self): """Return provider of the integration""" @@ -47,6 +48,7 @@ def __str__(self): return f"{self.workspace.name} <{self.integration.provider}>" class Meta: + unique_together = ["workspace", "integration"] verbose_name = "Workspace Integration" verbose_name_plural = "Workspace Integrations" db_table = "workspace_integrations" diff --git a/apiserver/plane/db/models/integration/github.py b/apiserver/plane/db/models/integration/github.py index a4d47a3f97e..0b76a85664e 100644 --- a/apiserver/plane/db/models/integration/github.py +++ b/apiserver/plane/db/models/integration/github.py @@ -5,7 +5,7 @@ from plane.db.models import ProjectBaseModel -class Repository(ProjectBaseModel): +class GithubRepository(ProjectBaseModel): name = models.CharField(max_length=500) url = models.URLField(null=True) config = models.JSONField(default=dict) @@ -22,7 +22,9 @@ class Meta: class GithubRepositorySync(ProjectBaseModel): - repository_id = models.BigIntegerField() + repository = models.ForeignKey( + "db.GithubRepository", on_delete=models.CASCADE, related_name="syncss" + ) credentials = models.JSONField(default=dict) def __str__(self): From 3177abcd9e726bd24091215e68e034a28565023d Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Wed, 8 Feb 2023 00:58:11 +0530 Subject: [PATCH 04/36] fix: update workspace integration view to create bot users --- apiserver/plane/api/views/integration/base.py | 3 +++ apiserver/plane/db/models/integration/github.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/apiserver/plane/api/views/integration/base.py b/apiserver/plane/api/views/integration/base.py index 8e7a3808788..764f179f205 100644 --- a/apiserver/plane/api/views/integration/base.py +++ b/apiserver/plane/api/views/integration/base.py @@ -3,6 +3,7 @@ # Django imports from django.db import IntegrityError +from django.contrib.auth.hashers import make_password # Third party imports from rest_framework.response import Response @@ -85,6 +86,8 @@ def create(self, request, slug): bot_user = User.objects.create( email=f"{str(integration.id)-str(workspace.id)}@plane.so", username=uuid.uuid4().hex, + password=make_password(uuid.uuid4().hex), + is_password_autoset=True, ) workspace_integration = WorkspaceIntegration.objects.create( diff --git a/apiserver/plane/db/models/integration/github.py b/apiserver/plane/db/models/integration/github.py index 0b76a85664e..627715d90cf 100644 --- a/apiserver/plane/db/models/integration/github.py +++ b/apiserver/plane/db/models/integration/github.py @@ -44,7 +44,7 @@ class GithubIssueSync(ProjectBaseModel): "db.Issue", related_name="github_syncs", on_delete=models.CASCADE ) repository = models.ForeignKey( - "db.Repository", related_name="issue_syncs", on_delete=models.CASCADE + "db.GithubRepository", related_name="issue_syncs", on_delete=models.CASCADE ) def __str__(self): From 20dc500035c88ae346c87929d125278172ad333e Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Wed, 8 Feb 2023 13:34:07 +0530 Subject: [PATCH 05/36] refactor: rename repository model --- apiserver/plane/api/urls.py | 20 ++++++++++++++++++- apiserver/plane/api/views/__init__.py | 2 +- .../plane/api/views/integration/__init__.py | 2 +- .../plane/api/views/integration/github.py | 2 +- .../plane/db/models/integration/github.py | 7 +++++++ 5 files changed, 29 insertions(+), 4 deletions(-) diff --git a/apiserver/plane/api/urls.py b/apiserver/plane/api/urls.py index 57c91921209..c0d1699c169 100644 --- a/apiserver/plane/api/urls.py +++ b/apiserver/plane/api/urls.py @@ -90,7 +90,7 @@ # Integrations IntegrationViewSet, WorkspaceIntegrationViewSet, - GithubRepoViewSet, + GithubRepositoryViewSet, GithubRepositorySyncViewSet, GithubIssueSyncViewSet, ## End Integrations @@ -735,5 +735,23 @@ ), name="workspace-integrations", ), + path( + "workspaces//integrations//github-repository/", + GithubRepositoryViewSet.as_view( + { + "get": "list", + "post": "create", + } + ), + ), + path( + "workspaces//integrations//github-repository//", + GithubRepositoryViewSet.as_view( + { + "get": "retrieve", + "delete": "destroy", + } + ), + ), ## End Integrations ] diff --git a/apiserver/plane/api/views/__init__.py b/apiserver/plane/api/views/__init__.py index 41145900b07..19627d3088e 100644 --- a/apiserver/plane/api/views/__init__.py +++ b/apiserver/plane/api/views/__init__.py @@ -80,5 +80,5 @@ IntegrationViewSet, GithubIssueSyncViewSet, GithubRepositorySyncViewSet, - GithubRepoViewSet, + GithubRepositoryViewSet, ) diff --git a/apiserver/plane/api/views/integration/__init__.py b/apiserver/plane/api/views/integration/__init__.py index fd6df0e1368..6ea65beb031 100644 --- a/apiserver/plane/api/views/integration/__init__.py +++ b/apiserver/plane/api/views/integration/__init__.py @@ -1,6 +1,6 @@ from .base import IntegrationViewSet, WorkspaceIntegrationViewSet from .github import ( - GithubRepoViewSet, + GithubRepositoryViewSet, GithubRepositorySyncViewSet, GithubIssueSyncViewSet, ) diff --git a/apiserver/plane/api/views/integration/github.py b/apiserver/plane/api/views/integration/github.py index aa8867053f1..df58d7efc73 100644 --- a/apiserver/plane/api/views/integration/github.py +++ b/apiserver/plane/api/views/integration/github.py @@ -11,7 +11,7 @@ ) -class GithubRepoViewSet(BaseViewSet): +class GithubRepositoryViewSet(BaseViewSet): serializer_class = GithubRepositorySerializer model = GithubRepository diff --git a/apiserver/plane/db/models/integration/github.py b/apiserver/plane/db/models/integration/github.py index 627715d90cf..dea3b64f684 100644 --- a/apiserver/plane/db/models/integration/github.py +++ b/apiserver/plane/db/models/integration/github.py @@ -26,6 +26,13 @@ class GithubRepositorySync(ProjectBaseModel): "db.GithubRepository", on_delete=models.CASCADE, related_name="syncss" ) credentials = models.JSONField(default=dict) + # Bot user + actor = models.ForeignKey( + "db.User", related_name="user_syncs", on_delete=models.CASCADE + ) + workspace_integration = models.ForeignKey( + "db.WorkspaceIntegration", related_name="github_syncs", on_delete=models.CASCADE + ) def __str__(self): """Return the repo sync""" From b75f2cec7b8070d57f676433e854818ed826cfb2 Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Wed, 8 Feb 2023 14:52:55 +0530 Subject: [PATCH 06/36] refactor: update github repo sync endpoint to create repo and sync in one go --- apiserver/plane/api/urls.py | 8 +-- .../plane/api/views/integration/github.py | 68 ++++++++++++++++--- apiserver/plane/db/models/integration/base.py | 2 +- .../plane/db/models/integration/github.py | 4 +- 4 files changed, 66 insertions(+), 16 deletions(-) diff --git a/apiserver/plane/api/urls.py b/apiserver/plane/api/urls.py index c0d1699c169..8f82cc56350 100644 --- a/apiserver/plane/api/urls.py +++ b/apiserver/plane/api/urls.py @@ -736,8 +736,8 @@ name="workspace-integrations", ), path( - "workspaces//integrations//github-repository/", - GithubRepositoryViewSet.as_view( + "workspaces//workspace-integrations//github-repository-sync/", + GithubRepositorySyncViewSet.as_view( { "get": "list", "post": "create", @@ -745,8 +745,8 @@ ), ), path( - "workspaces//integrations//github-repository//", - GithubRepositoryViewSet.as_view( + "workspaces//workspace-integrations//github-repository-sync//", + GithubRepositorySyncViewSet.as_view( { "get": "retrieve", "delete": "destroy", diff --git a/apiserver/plane/api/views/integration/github.py b/apiserver/plane/api/views/integration/github.py index df58d7efc73..66494c397a1 100644 --- a/apiserver/plane/api/views/integration/github.py +++ b/apiserver/plane/api/views/integration/github.py @@ -1,24 +1,23 @@ # Third party imports from rest_framework import status +from rest_framework.response import Response +from sentry_sdk import capture_exception # Module imports from plane.api.views import BaseViewSet -from plane.db.models import GithubIssueSync, GithubRepositorySync, GithubRepository +from plane.db.models import ( + GithubIssueSync, + GithubRepositorySync, + GithubRepository, + WorkspaceIntegration, +) from plane.api.serializers import ( + GithubRepositorySerializer, GithubIssueSyncSerializer, GithubRepositorySyncSerializer, - GithubRepositorySerializer, ) -class GithubRepositoryViewSet(BaseViewSet): - serializer_class = GithubRepositorySerializer - model = GithubRepository - - def perform_create(self, serializer): - serializer.save(project_id=self.kwargs.get("project_id")) - - class GithubRepositorySyncViewSet(BaseViewSet): serializer_class = GithubRepositorySyncSerializer model = GithubRepositorySync @@ -26,6 +25,55 @@ class GithubRepositorySyncViewSet(BaseViewSet): def perform_create(self, serializer): serializer.save(project_id=self.kwargs.get("project_id")) + def create(self, request, workspace_integration_id): + try: + name = (request.data.get("name", False),) + url = (request.data.get("url", False),) + config = (request.data.get("config", {}),) + repository_id = request.data.get("repository_id", False) + + if not name or not url or not repository_id: + return Response( + {"error": "Name, url, and repository_id are required"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + # Create repository + repo = GithubRepository.objects.create( + name=name, url=url, config=config, repository_id=repository_id + ) + + # Get the workspace integration + workspace_integration = WorkspaceIntegration.objects.get( + pk=workspace_integration_id + ) + + # Create repo sync + repo_sync = GithubRepositorySync.objects.create( + repository=repo, + workspace_integration=workspace_integration, + actor=workspace_integration.actor, + credetials=request.data.get("credentials", {}), + ) + + # Return Response + return Response( + GithubRepositorySyncSerializer(repo_sync).data, + status=status.HTTP_201_CREATED, + ) + + except WorkspaceIntegration.DoesNotExist: + return Response( + {"error": "Workspace Integration does not exist"}, + status=status.HTTP_404_NOT_FOUND, + ) + except Exception as e: + capture_exception(e) + return Response( + {"error": "Something went wrong please try again later"}, + status=status.HTTP_400_BAD_REQUEST, + ) + class GithubIssueSyncViewSet(BaseViewSet): serializer_class = GithubIssueSyncSerializer diff --git a/apiserver/plane/db/models/integration/base.py b/apiserver/plane/db/models/integration/base.py index e2d5486fa43..e9bda13597a 100644 --- a/apiserver/plane/db/models/integration/base.py +++ b/apiserver/plane/db/models/integration/base.py @@ -35,7 +35,7 @@ class WorkspaceIntegration(BaseModel): "db.Workspace", related_name="workspace_integrations", on_delete=models.CASCADE ) # Bot user - user = models.ForeignKey( + actor = models.ForeignKey( "db.User", related_name="integrations", on_delete=models.CASCADE ) integration = models.ForeignKey( diff --git a/apiserver/plane/db/models/integration/github.py b/apiserver/plane/db/models/integration/github.py index dea3b64f684..e2c6ffa69a4 100644 --- a/apiserver/plane/db/models/integration/github.py +++ b/apiserver/plane/db/models/integration/github.py @@ -3,12 +3,14 @@ # Module imports from plane.db.models import ProjectBaseModel +from plane.db.mixins import AuditModel -class GithubRepository(ProjectBaseModel): +class GithubRepository(AuditModel): name = models.CharField(max_length=500) url = models.URLField(null=True) config = models.JSONField(default=dict) + repository_id = models.BigIntegerField() def __str__(self): """Return the repo name""" From 31c06152e56eaff0028d870d10657f1c11a7ddda Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Wed, 8 Feb 2023 15:33:00 +0530 Subject: [PATCH 07/36] refactor: update issue activities to post the updates to segway hook --- apiserver/plane/api/views/issue.py | 30 ++++------- .../plane/bgtasks/issue_activites_task.py | 51 ++++++++++++++----- apiserver/plane/settings/local.py | 1 + apiserver/plane/settings/production.py | 2 + apiserver/plane/settings/staging.py | 2 + 5 files changed, 54 insertions(+), 32 deletions(-) diff --git a/apiserver/plane/api/views/issue.py b/apiserver/plane/api/views/issue.py index 37082e0ec8a..acfce2409b2 100644 --- a/apiserver/plane/api/views/issue.py +++ b/apiserver/plane/api/views/issue.py @@ -75,7 +75,6 @@ def perform_update(self, serializer): self.get_queryset().filter(pk=self.kwargs.get("pk", None)).first() ) if current_instance is not None: - issue_activity.delay( { "type": "issue.activity", @@ -92,7 +91,6 @@ def perform_update(self, serializer): return super().perform_update(serializer) def get_queryset(self): - return ( super() .get_queryset() @@ -202,15 +200,18 @@ def create(self, request, slug, project_id): serializer.save() # Track the issue - IssueActivity.objects.create( - issue_id=serializer.data["id"], - project_id=project_id, - workspace_id=serializer["workspace"], - comment=f"{request.user.email} created the issue", - verb="created", - actor=request.user, + issue_activity.delay( + { + "type": "issue.activity", + "requested_data": json.dumps( + self.request.data, cls=DjangoJSONEncoder + ), + "actor_id": str(request.user.id), + "issue_id": str(serializer.data.get("id", None)), + "project_id": str(project_id), + "current_instance": None, + }, ) - return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) @@ -277,7 +278,6 @@ def get(self, request, slug): class WorkSpaceIssuesEndpoint(BaseAPIView): - permission_classes = [ WorkSpaceAdminPermission, ] @@ -298,7 +298,6 @@ def get(self, request, slug): class IssueActivityEndpoint(BaseAPIView): - permission_classes = [ ProjectEntityPermission, ] @@ -333,7 +332,6 @@ def get(self, request, slug, project_id, issue_id): class IssueCommentViewSet(BaseViewSet): - serializer_class = IssueCommentSerializer model = IssueComment permission_classes = [ @@ -436,7 +434,6 @@ def list(self, request, slug, project_id): def create(self, request, slug, project_id): try: - issue_property, created = IssueProperty.objects.get_or_create( user=request.user, project_id=project_id, @@ -463,7 +460,6 @@ def create(self, request, slug, project_id): class LabelViewSet(BaseViewSet): - serializer_class = LabelSerializer model = Label permission_classes = [ @@ -490,14 +486,12 @@ def get_queryset(self): class BulkDeleteIssuesEndpoint(BaseAPIView): - permission_classes = [ ProjectEntityPermission, ] def delete(self, request, slug, project_id): try: - issue_ids = request.data.get("issue_ids", []) if not len(issue_ids): @@ -527,14 +521,12 @@ def delete(self, request, slug, project_id): class SubIssuesEndpoint(BaseAPIView): - permission_classes = [ ProjectEntityPermission, ] def get(self, request, slug, project_id, issue_id): try: - sub_issues = ( Issue.objects.filter( parent_id=issue_id, workspace__slug=slug, project_id=project_id diff --git a/apiserver/plane/bgtasks/issue_activites_task.py b/apiserver/plane/bgtasks/issue_activites_task.py index 7e0e3f6ffd0..a666b42af15 100644 --- a/apiserver/plane/bgtasks/issue_activites_task.py +++ b/apiserver/plane/bgtasks/issue_activites_task.py @@ -1,5 +1,9 @@ # Python imports import json +import requests + +# Django imports +from django.conf import settings # Third Party imports from django_rq import job @@ -16,6 +20,7 @@ Cycle, Module, ) +from plane.api.serializers import IssueActivitySerializer # Track Chnages in name @@ -644,21 +649,41 @@ def issue_activity(event): "modules_list": track_modules, } - for key in requested_data: - func = ISSUE_ACTIVITY_MAPPER.get(key, None) - if func is not None: - func( - requested_data, - current_instance, - issue_id, - project, - actor, - issue_activities, - ) + if current_instance is None: + issue_activities.append( + issue_id=issue_id, + project=project, + workspace=project.workspace, + comment=f"{actor.email} created the issue", + verb="created", + actor=actor, + ) + else: + for key in requested_data: + func = ISSUE_ACTIVITY_MAPPER.get(key, None) + if func is not None: + func( + requested_data, + current_instance, + issue_id, + project, + actor, + issue_activities, + ) # Save all the values to database - _ = IssueActivity.objects.bulk_create(issue_activities) - + issue_activities_created = IssueActivity.objects.bulk_create(issue_activities) + + # Post the updates to segway for integrations and webhooks + if settings.PROXY_BASE_URL: + for issue_activity in issue_activities_created: + headers = {"Content-Type": "application/json"} + issue_activity_json = IssueActivitySerializer(issue_activity).data + _ = requests.post( + f"{settings.PROXY_BASE_URL}/issue-activity-hooks/", + json=issue_activity_json, + headers=headers, + ) return except Exception as e: capture_exception(e) diff --git a/apiserver/plane/settings/local.py b/apiserver/plane/settings/local.py index 4d4af9b77d8..bf62c8a261d 100644 --- a/apiserver/plane/settings/local.py +++ b/apiserver/plane/settings/local.py @@ -66,3 +66,4 @@ WEB_URL = "http://localhost:3000" +PROXY_BASE_URL = os.environ.get("PROXY_BASE_URL", False) diff --git a/apiserver/plane/settings/production.py b/apiserver/plane/settings/production.py index c8390148464..c9fae7aa0bc 100644 --- a/apiserver/plane/settings/production.py +++ b/apiserver/plane/settings/production.py @@ -190,3 +190,5 @@ ) # Set the variable true if running in docker-compose environment WEB_URL = os.environ.get("WEB_URL") + +PROXY_BASE_URL = os.environ.get("PROXY_BASE_URL", False) diff --git a/apiserver/plane/settings/staging.py b/apiserver/plane/settings/staging.py index 725f2cd850f..0e58ab224de 100644 --- a/apiserver/plane/settings/staging.py +++ b/apiserver/plane/settings/staging.py @@ -185,3 +185,5 @@ WEB_URL = os.environ.get("WEB_URL") + +PROXY_BASE_URL = os.environ.get("PROXY_BASE_URL", False) From 3d845ccf364258aabda216298cf28be401421952 Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Wed, 8 Feb 2023 23:29:23 +0530 Subject: [PATCH 08/36] refactor: update endpoints to get project id and add actor as a member of project in repo sync --- apiserver/plane/api/urls.py | 4 ++-- apiserver/plane/api/views/integration/base.py | 16 ++++++++++++++-- apiserver/plane/api/views/integration/github.py | 10 +++++++++- 3 files changed, 25 insertions(+), 5 deletions(-) diff --git a/apiserver/plane/api/urls.py b/apiserver/plane/api/urls.py index 8f82cc56350..4f8617c50e5 100644 --- a/apiserver/plane/api/urls.py +++ b/apiserver/plane/api/urls.py @@ -736,7 +736,7 @@ name="workspace-integrations", ), path( - "workspaces//workspace-integrations//github-repository-sync/", + "workspaces//projects//workspace-integrations//github-repository-sync/", GithubRepositorySyncViewSet.as_view( { "get": "list", @@ -745,7 +745,7 @@ ), ), path( - "workspaces//workspace-integrations//github-repository-sync//", + "workspaces//projects//workspace-integrations//github-repository-sync//", GithubRepositorySyncViewSet.as_view( { "get": "retrieve", diff --git a/apiserver/plane/api/views/integration/base.py b/apiserver/plane/api/views/integration/base.py index 764f179f205..bf276eb9e76 100644 --- a/apiserver/plane/api/views/integration/base.py +++ b/apiserver/plane/api/views/integration/base.py @@ -12,7 +12,13 @@ # Module imports from plane.api.views import BaseViewSet -from plane.db.models import Integration, WorkspaceIntegration, Workspace, User +from plane.db.models import ( + Integration, + WorkspaceIntegration, + Workspace, + User, + WorkspaceMember, +) from plane.api.serializers import IntegrationSerializer, WorkspaceIntegrationSerializer @@ -84,7 +90,7 @@ def create(self, request, slug): # Create a bot user bot_user = User.objects.create( - email=f"{str(integration.id)-str(workspace.id)}@plane.so", + email=f"{str(integration.id)}-{str(workspace.id)}@plane.so", username=uuid.uuid4().hex, password=make_password(uuid.uuid4().hex), is_password_autoset=True, @@ -96,6 +102,12 @@ def create(self, request, slug): user=bot_user, ) + # Add bot user as a member of workspace + _ = WorkspaceMember.objects.create( + workspace=workspace_integration.workspace, + member=bot_user, + role=20, + ) return Response( WorkspaceIntegrationSerializer(workspace_integration).data, status=status.HTTP_201_CREATED, diff --git a/apiserver/plane/api/views/integration/github.py b/apiserver/plane/api/views/integration/github.py index 66494c397a1..98f23c9ff75 100644 --- a/apiserver/plane/api/views/integration/github.py +++ b/apiserver/plane/api/views/integration/github.py @@ -10,6 +10,7 @@ GithubRepositorySync, GithubRepository, WorkspaceIntegration, + ProjectMember, ) from plane.api.serializers import ( GithubRepositorySerializer, @@ -25,7 +26,7 @@ class GithubRepositorySyncViewSet(BaseViewSet): def perform_create(self, serializer): serializer.save(project_id=self.kwargs.get("project_id")) - def create(self, request, workspace_integration_id): + def create(self, request, slug, project_id, workspace_integration_id): try: name = (request.data.get("name", False),) url = (request.data.get("url", False),) @@ -54,6 +55,13 @@ def create(self, request, workspace_integration_id): workspace_integration=workspace_integration, actor=workspace_integration.actor, credetials=request.data.get("credentials", {}), + project_id=project_id, + ) + + # Add bot as a member in the project + _ = ProjectMember.objects.create( + member=workspace_integration.actor, + role=20, ) # Return Response From 10cd9a7ad685cfe562ed75df3857f2b2068dbf5f Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Wed, 8 Feb 2023 23:36:24 +0530 Subject: [PATCH 09/36] fix: make is bot as a read only field --- apiserver/plane/api/serializers/user.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apiserver/plane/api/serializers/user.py b/apiserver/plane/api/serializers/user.py index 808991ddc0b..dea0738cced 100644 --- a/apiserver/plane/api/serializers/user.py +++ b/apiserver/plane/api/serializers/user.py @@ -21,6 +21,7 @@ class Meta: "last_login_uagent", "token_updated_at", "is_onboarded", + "is_bot", ] extra_kwargs = {"password": {"write_only": True}} @@ -37,4 +38,5 @@ class Meta: ] read_only_fields = [ "id", + "is_bot", ] From 387b7ebe85fb2150183fc6a9e25b6115ab27c53f Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Thu, 9 Feb 2023 00:19:21 +0530 Subject: [PATCH 10/36] fix: remove github repo imports --- apiserver/plane/api/urls.py | 1 - apiserver/plane/api/views/__init__.py | 1 - apiserver/plane/api/views/integration/__init__.py | 1 - 3 files changed, 3 deletions(-) diff --git a/apiserver/plane/api/urls.py b/apiserver/plane/api/urls.py index 4f8617c50e5..4e803cdb934 100644 --- a/apiserver/plane/api/urls.py +++ b/apiserver/plane/api/urls.py @@ -90,7 +90,6 @@ # Integrations IntegrationViewSet, WorkspaceIntegrationViewSet, - GithubRepositoryViewSet, GithubRepositorySyncViewSet, GithubIssueSyncViewSet, ## End Integrations diff --git a/apiserver/plane/api/views/__init__.py b/apiserver/plane/api/views/__init__.py index 19627d3088e..c21399bb449 100644 --- a/apiserver/plane/api/views/__init__.py +++ b/apiserver/plane/api/views/__init__.py @@ -80,5 +80,4 @@ IntegrationViewSet, GithubIssueSyncViewSet, GithubRepositorySyncViewSet, - GithubRepositoryViewSet, ) diff --git a/apiserver/plane/api/views/integration/__init__.py b/apiserver/plane/api/views/integration/__init__.py index 6ea65beb031..29d8d1ffec9 100644 --- a/apiserver/plane/api/views/integration/__init__.py +++ b/apiserver/plane/api/views/integration/__init__.py @@ -1,6 +1,5 @@ from .base import IntegrationViewSet, WorkspaceIntegrationViewSet from .github import ( - GithubRepositoryViewSet, GithubRepositorySyncViewSet, GithubIssueSyncViewSet, ) From a776900f3bf6c6f7a29b27dd96632ee51c500ce5 Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Thu, 9 Feb 2023 00:20:49 +0530 Subject: [PATCH 11/36] fix: url mapping --- apiserver/plane/api/urls.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apiserver/plane/api/urls.py b/apiserver/plane/api/urls.py index 4e803cdb934..8baa9e84dc1 100644 --- a/apiserver/plane/api/urls.py +++ b/apiserver/plane/api/urls.py @@ -735,7 +735,7 @@ name="workspace-integrations", ), path( - "workspaces//projects//workspace-integrations//github-repository-sync/", + "workspaces//projects//workspace-integrations//github-repository-sync/", GithubRepositorySyncViewSet.as_view( { "get": "list", @@ -744,7 +744,7 @@ ), ), path( - "workspaces//projects//workspace-integrations//github-repository-sync//", + "workspaces//projects//workspace-integrations//github-repository-sync//", GithubRepositorySyncViewSet.as_view( { "get": "retrieve", From ae67982ee4a56767c92c0727dea57bf50c72ac09 Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Thu, 9 Feb 2023 00:40:21 +0530 Subject: [PATCH 12/36] feat: repo views --- apiserver/plane/api/urls.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/apiserver/plane/api/urls.py b/apiserver/plane/api/urls.py index 8baa9e84dc1..d4f68f66c4f 100644 --- a/apiserver/plane/api/urls.py +++ b/apiserver/plane/api/urls.py @@ -734,6 +734,7 @@ ), name="workspace-integrations", ), + # Github Integrations path( "workspaces//projects//workspace-integrations//github-repository-sync/", GithubRepositorySyncViewSet.as_view( @@ -744,7 +745,16 @@ ), ), path( - "workspaces//projects//workspace-integrations//github-repository-sync//", + "workspaces//projects//github-repository-sync//github-sync-issues/", + GithubIssueSyncViewSet.as_view( + { + "post": "create", + "get": "list", + } + ), + ), + path( + "workspaces//projects//github-repository-sync//github-sync-issues//", GithubRepositorySyncViewSet.as_view( { "get": "retrieve", @@ -752,5 +762,6 @@ } ), ), + ## End Github Integrations ## End Integrations ] From 5e4245ce90de1bb74b7a8646246ea6f11f568e79 Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Thu, 9 Feb 2023 02:52:01 +0530 Subject: [PATCH 13/36] refactor: update webhook request endpoint --- apiserver/plane/bgtasks/issue_activites_task.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/apiserver/plane/bgtasks/issue_activites_task.py b/apiserver/plane/bgtasks/issue_activites_task.py index a666b42af15..375972aaecb 100644 --- a/apiserver/plane/bgtasks/issue_activites_task.py +++ b/apiserver/plane/bgtasks/issue_activites_task.py @@ -4,6 +4,7 @@ # Django imports from django.conf import settings +from django.core.serializers.json import DjangoJSONEncoder # Third Party imports from django_rq import job @@ -673,14 +674,15 @@ def issue_activity(event): # Save all the values to database issue_activities_created = IssueActivity.objects.bulk_create(issue_activities) - # Post the updates to segway for integrations and webhooks if settings.PROXY_BASE_URL: for issue_activity in issue_activities_created: headers = {"Content-Type": "application/json"} - issue_activity_json = IssueActivitySerializer(issue_activity).data + issue_activity_json = json.dumps( + IssueActivitySerializer(issue_activity).data, cls=DjangoJSONEncoder + ) _ = requests.post( - f"{settings.PROXY_BASE_URL}/issue-activity-hooks/", + f"{settings.PROXY_BASE_URL}/hooks/workspaces/{str(issue_activity.workspace_id)}/projects/{str(issue_activity.project_id)}/issues/{str(issue_activity.issue_id)}/issue-activity-hooks/", json=issue_activity_json, headers=headers, ) From 45194efd98590dc9b166e568fce1113e2068e221 Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Fri, 10 Feb 2023 02:25:56 +0530 Subject: [PATCH 14/36] refactor: rename repositories table to github_repositories --- apiserver/plane/db/models/integration/github.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apiserver/plane/db/models/integration/github.py b/apiserver/plane/db/models/integration/github.py index e2c6ffa69a4..03b44ccd977 100644 --- a/apiserver/plane/db/models/integration/github.py +++ b/apiserver/plane/db/models/integration/github.py @@ -19,7 +19,7 @@ def __str__(self): class Meta: verbose_name = "Repository" verbose_name_plural = "Repositories" - db_table = "repositories" + db_table = "github_repositories" ordering = ("-created_at",) From f5b6f9528a2e952f82b8e7ea46ccc185de444ec0 Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Fri, 10 Feb 2023 21:04:00 +0530 Subject: [PATCH 15/36] fix: workpace integration actor --- apiserver/plane/api/urls.py | 2 +- apiserver/plane/api/views/integration/base.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/apiserver/plane/api/urls.py b/apiserver/plane/api/urls.py index d4f68f66c4f..027805e4cf1 100644 --- a/apiserver/plane/api/urls.py +++ b/apiserver/plane/api/urls.py @@ -704,7 +704,7 @@ name="integrations", ), path( - "integrations//", + "integrations//", IntegrationViewSet.as_view( { "get": "retrieve", diff --git a/apiserver/plane/api/views/integration/base.py b/apiserver/plane/api/views/integration/base.py index bf276eb9e76..3a5e507e81c 100644 --- a/apiserver/plane/api/views/integration/base.py +++ b/apiserver/plane/api/views/integration/base.py @@ -94,12 +94,13 @@ def create(self, request, slug): username=uuid.uuid4().hex, password=make_password(uuid.uuid4().hex), is_password_autoset=True, + is_bot=True, ) workspace_integration = WorkspaceIntegration.objects.create( workspace=workspace, integration=integration, - user=bot_user, + actor=bot_user, ) # Add bot user as a member of workspace From 738d2f5d0f0a20fd292d3f2f20ea66196a23417e Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Fri, 10 Feb 2023 22:13:04 +0530 Subject: [PATCH 16/36] feat: label for github integration --- apiserver/plane/api/views/integration/github.py | 10 ++++++++++ apiserver/plane/db/models/integration/github.py | 3 +++ 2 files changed, 13 insertions(+) diff --git a/apiserver/plane/api/views/integration/github.py b/apiserver/plane/api/views/integration/github.py index 98f23c9ff75..d568180e2cc 100644 --- a/apiserver/plane/api/views/integration/github.py +++ b/apiserver/plane/api/views/integration/github.py @@ -11,6 +11,7 @@ GithubRepository, WorkspaceIntegration, ProjectMember, + Label, ) from plane.api.serializers import ( GithubRepositorySerializer, @@ -49,6 +50,14 @@ def create(self, request, slug, project_id, workspace_integration_id): pk=workspace_integration_id ) + # Create a Label for github + label = Label.objects.create( + name="GitHub", + description="Label to sync Plane issues with GitHub issues", + color="#003773", + project_id=project_id, + ) + # Create repo sync repo_sync = GithubRepositorySync.objects.create( repository=repo, @@ -56,6 +65,7 @@ def create(self, request, slug, project_id, workspace_integration_id): actor=workspace_integration.actor, credetials=request.data.get("credentials", {}), project_id=project_id, + label=label, ) # Add bot as a member in the project diff --git a/apiserver/plane/db/models/integration/github.py b/apiserver/plane/db/models/integration/github.py index 03b44ccd977..9592829c8bb 100644 --- a/apiserver/plane/db/models/integration/github.py +++ b/apiserver/plane/db/models/integration/github.py @@ -35,6 +35,9 @@ class GithubRepositorySync(ProjectBaseModel): workspace_integration = models.ForeignKey( "db.WorkspaceIntegration", related_name="github_syncs", on_delete=models.CASCADE ) + label = models.ForeignKey( + "db.Label", on_delete=models.SET_NULL, null=True, related_name="repo_syncs" + ) def __str__(self): """Return the repo sync""" From 0e7204b4d9ef4625860a75a139a2cc784ce9cb0c Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Fri, 10 Feb 2023 22:34:08 +0530 Subject: [PATCH 17/36] refactor: issue activity on create issue --- .../plane/bgtasks/issue_activites_task.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/apiserver/plane/bgtasks/issue_activites_task.py b/apiserver/plane/bgtasks/issue_activites_task.py index 375972aaecb..997e47e7683 100644 --- a/apiserver/plane/bgtasks/issue_activites_task.py +++ b/apiserver/plane/bgtasks/issue_activites_task.py @@ -623,7 +623,7 @@ def track_modules( def issue_activity(event): try: issue_activities = [] - + type = event.get("type") requested_data = json.loads(event.get("requested_data")) current_instance = json.loads(event.get("current_instance")) issue_id = event.get("issue_id", None) @@ -651,14 +651,15 @@ def issue_activity(event): } if current_instance is None: - issue_activities.append( - issue_id=issue_id, - project=project, - workspace=project.workspace, - comment=f"{actor.email} created the issue", - verb="created", - actor=actor, - ) + if type == "issue.activity": + issue_activities.append( + issue_id=issue_id, + project=project, + workspace=project.workspace, + comment=f"{actor.email} created the issue", + verb="created", + actor=actor, + ) else: for key in requested_data: func = ISSUE_ACTIVITY_MAPPER.get(key, None) From 940bdf9411d9e84c5543230bb2927950a20e8b5d Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Sat, 11 Feb 2023 16:48:50 +0530 Subject: [PATCH 18/36] refactor: repo create endpoint and add db constraints for repo sync and issues --- .../api/serializers/integration/github.py | 7 ++++ apiserver/plane/api/urls.py | 4 +- .../plane/api/views/integration/github.py | 39 ++++++++++++------- .../plane/bgtasks/issue_activites_task.py | 20 ++++++---- .../plane/db/models/integration/github.py | 12 ++++-- 5 files changed, 56 insertions(+), 26 deletions(-) diff --git a/apiserver/plane/api/serializers/integration/github.py b/apiserver/plane/api/serializers/integration/github.py index 6279b0624b1..66c310346dd 100644 --- a/apiserver/plane/api/serializers/integration/github.py +++ b/apiserver/plane/api/serializers/integration/github.py @@ -10,6 +10,8 @@ class Meta: class GithubRepositorySyncSerializer(BaseSerializer): + repo_detail = GithubRepositorySerializer(source="repository") + class Meta: model = GithubRepositorySync fields = "__all__" @@ -19,3 +21,8 @@ class GithubIssueSyncSerializer(BaseSerializer): class Meta: model = GithubIssueSync fields = "__all__" + read_only_fields = [ + "project", + "workspace", + "repository_sync", + ] diff --git a/apiserver/plane/api/urls.py b/apiserver/plane/api/urls.py index 027805e4cf1..ccedc4e1d6a 100644 --- a/apiserver/plane/api/urls.py +++ b/apiserver/plane/api/urls.py @@ -745,7 +745,7 @@ ), ), path( - "workspaces//projects//github-repository-sync//github-sync-issues/", + "workspaces//projects//github-repository-sync//github-issue-sync/", GithubIssueSyncViewSet.as_view( { "post": "create", @@ -754,7 +754,7 @@ ), ), path( - "workspaces//projects//github-repository-sync//github-sync-issues//", + "workspaces//projects//github-repository-sync//github-issue-sync//", GithubRepositorySyncViewSet.as_view( { "get": "retrieve", diff --git a/apiserver/plane/api/views/integration/github.py b/apiserver/plane/api/views/integration/github.py index d568180e2cc..a95d055d5d7 100644 --- a/apiserver/plane/api/views/integration/github.py +++ b/apiserver/plane/api/views/integration/github.py @@ -29,12 +29,13 @@ def perform_create(self, serializer): def create(self, request, slug, project_id, workspace_integration_id): try: - name = (request.data.get("name", False),) - url = (request.data.get("url", False),) - config = (request.data.get("config", {}),) + name = request.data.get("name", False) + url = request.data.get("url", False) + config = request.data.get("config", {}) repository_id = request.data.get("repository_id", False) + owner = request.data.get("owner", False) - if not name or not url or not repository_id: + if not name or not url or not repository_id or not owner: return Response( {"error": "Name, url, and repository_id are required"}, status=status.HTTP_400_BAD_REQUEST, @@ -42,7 +43,11 @@ def create(self, request, slug, project_id, workspace_integration_id): # Create repository repo = GithubRepository.objects.create( - name=name, url=url, config=config, repository_id=repository_id + name=name, + url=url, + config=config, + repository_id=repository_id, + owner=owner, ) # Get the workspace integration @@ -51,27 +56,32 @@ def create(self, request, slug, project_id, workspace_integration_id): ) # Create a Label for github - label = Label.objects.create( + label = Label.objects.filter( name="GitHub", - description="Label to sync Plane issues with GitHub issues", - color="#003773", project_id=project_id, - ) + ).first() + + if label is None: + label = Label.objects.create( + name="GitHub", + project_id=project_id, + description="Label to sync Plane issues with GitHub issues", + color="#003773", + ) # Create repo sync repo_sync = GithubRepositorySync.objects.create( repository=repo, workspace_integration=workspace_integration, actor=workspace_integration.actor, - credetials=request.data.get("credentials", {}), + credentials=request.data.get("credentials", {}), project_id=project_id, label=label, ) # Add bot as a member in the project _ = ProjectMember.objects.create( - member=workspace_integration.actor, - role=20, + member=workspace_integration.actor, role=20, project_id=project_id ) # Return Response @@ -98,4 +108,7 @@ class GithubIssueSyncViewSet(BaseViewSet): model = GithubIssueSync def perform_create(self, serializer): - serializer.save(project_id=self.kwargs.get("project_id")) + serializer.save( + project_id=self.kwargs.get("project_id"), + repository_sync_id=self.kwargs.get("repo_sync_id"), + ) diff --git a/apiserver/plane/bgtasks/issue_activites_task.py b/apiserver/plane/bgtasks/issue_activites_task.py index 997e47e7683..dfd32897dd3 100644 --- a/apiserver/plane/bgtasks/issue_activites_task.py +++ b/apiserver/plane/bgtasks/issue_activites_task.py @@ -625,7 +625,11 @@ def issue_activity(event): issue_activities = [] type = event.get("type") requested_data = json.loads(event.get("requested_data")) - current_instance = json.loads(event.get("current_instance")) + current_instance = ( + json.loads(event.get("current_instance")) + if event.get("current_instance") is not None + else None + ) issue_id = event.get("issue_id", None) actor_id = event.get("actor_id") project_id = event.get("project_id") @@ -653,12 +657,14 @@ def issue_activity(event): if current_instance is None: if type == "issue.activity": issue_activities.append( - issue_id=issue_id, - project=project, - workspace=project.workspace, - comment=f"{actor.email} created the issue", - verb="created", - actor=actor, + IssueActivity( + issue_id=issue_id, + project=project, + workspace=project.workspace, + comment=f"{actor.email} created the issue", + verb="created", + actor=actor, + ) ) else: for key in requested_data: diff --git a/apiserver/plane/db/models/integration/github.py b/apiserver/plane/db/models/integration/github.py index 9592829c8bb..06362eee8fb 100644 --- a/apiserver/plane/db/models/integration/github.py +++ b/apiserver/plane/db/models/integration/github.py @@ -11,6 +11,7 @@ class GithubRepository(AuditModel): url = models.URLField(null=True) config = models.JSONField(default=dict) repository_id = models.BigIntegerField() + owner = models.CharField(max_length=500) def __str__(self): """Return the repo name""" @@ -24,8 +25,8 @@ class Meta: class GithubRepositorySync(ProjectBaseModel): - repository = models.ForeignKey( - "db.GithubRepository", on_delete=models.CASCADE, related_name="syncss" + repository = models.OneToOneField( + "db.GithubRepository", on_delete=models.CASCADE, related_name="syncs" ) credentials = models.JSONField(default=dict) # Bot user @@ -44,6 +45,7 @@ def __str__(self): return f"{self.repository.name} <{self.project.name}>" class Meta: + unique_together = ["project", "repository"] verbose_name = "Github Repository Sync" verbose_name_plural = "Github Repository Syncs" db_table = "github_repository_syncs" @@ -51,12 +53,13 @@ class Meta: class GithubIssueSync(ProjectBaseModel): + repo_issue_id = models.BigIntegerField() github_issue_id = models.BigIntegerField() issue = models.ForeignKey( "db.Issue", related_name="github_syncs", on_delete=models.CASCADE ) - repository = models.ForeignKey( - "db.GithubRepository", related_name="issue_syncs", on_delete=models.CASCADE + repository_sync = models.ForeignKey( + "db.GithubRepositorySync", related_name="issue_syncs", on_delete=models.CASCADE ) def __str__(self): @@ -64,6 +67,7 @@ def __str__(self): return f"{self.repository.name}-{self.project.name}-{self.issue.name}" class Meta: + unique_together = ["repository_sync", "issue"] verbose_name = "Github Issue Sync" verbose_name_plural = "Github Issue Syncs" db_table = "github_issue_syncs" From 6c94af6503c9f48a623af2825f1ada9c95d2b501 Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Sat, 11 Feb 2023 23:12:13 +0530 Subject: [PATCH 19/36] feat: create api token on workpsace integration and avatar_url for integrations --- apiserver/plane/api/views/integration/base.py | 11 +++++++++++ apiserver/plane/db/models/integration/base.py | 4 ++++ 2 files changed, 15 insertions(+) diff --git a/apiserver/plane/api/views/integration/base.py b/apiserver/plane/api/views/integration/base.py index 3a5e507e81c..001ced857aa 100644 --- a/apiserver/plane/api/views/integration/base.py +++ b/apiserver/plane/api/views/integration/base.py @@ -18,6 +18,7 @@ Workspace, User, WorkspaceMember, + APIToken, ) from plane.api.serializers import IntegrationSerializer, WorkspaceIntegrationSerializer @@ -95,12 +96,22 @@ def create(self, request, slug): password=make_password(uuid.uuid4().hex), is_password_autoset=True, is_bot=True, + first_name=integration.provider, + avatar=integration.avatar_url, + ) + + # Create an API Token for the bot user + api_token = APIToken.objects.create( + user=bot_user, + user_type=1, # bot user + workspace=workspace, ) workspace_integration = WorkspaceIntegration.objects.create( workspace=workspace, integration=integration, actor=bot_user, + api_token=api_token, ) # Add bot user as a member of workspace diff --git a/apiserver/plane/db/models/integration/base.py b/apiserver/plane/db/models/integration/base.py index e9bda13597a..d25a7477487 100644 --- a/apiserver/plane/db/models/integration/base.py +++ b/apiserver/plane/db/models/integration/base.py @@ -18,6 +18,7 @@ class Integration(AuditModel): redirect_url = models.TextField(blank=True) metadata = models.JSONField(default=dict) verified = models.BooleanField(default=False) + avatar_url = models.URLField(blank=True, null=True) def __str__(self): """Return provider of the integration""" @@ -41,6 +42,9 @@ class WorkspaceIntegration(BaseModel): integration = models.ForeignKey( "db.Integration", related_name="integrated_workspaces", on_delete=models.CASCADE ) + api_token = models.ForeignKey( + "db.APIToken", related_name="integrations", on_delete=models.CASCADE + ) metadata = models.JSONField(default=dict) def __str__(self): From 39ef1a3800e6a1bee746cc0bcb903ebfe56d154b Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Sat, 11 Feb 2023 23:14:31 +0530 Subject: [PATCH 20/36] refactor: add uuid primary key for Audit model --- apiserver/plane/api/urls.py | 2 +- apiserver/plane/db/mixins.py | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/apiserver/plane/api/urls.py b/apiserver/plane/api/urls.py index ccedc4e1d6a..97b23e04d8f 100644 --- a/apiserver/plane/api/urls.py +++ b/apiserver/plane/api/urls.py @@ -704,7 +704,7 @@ name="integrations", ), path( - "integrations//", + "integrations//", IntegrationViewSet.as_view( { "get": "retrieve", diff --git a/apiserver/plane/db/mixins.py b/apiserver/plane/db/mixins.py index b48e5c96552..3b1b7fe87b1 100644 --- a/apiserver/plane/db/mixins.py +++ b/apiserver/plane/db/mixins.py @@ -1,3 +1,7 @@ +# Python imports +import uuid + +# Django imports from django.db import models @@ -42,5 +46,9 @@ class AuditModel(TimeAuditModel, UserAuditModel): """To path when the record was created and last modified""" + id = models.UUIDField( + default=uuid.uuid4, unique=True, editable=False, db_index=True, primary_key=True + ) + class Meta: abstract = True From a2ab93ccd9f9375079b2741234cd049adceadd39 Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Sun, 12 Feb 2023 00:07:21 +0530 Subject: [PATCH 21/36] refactor: remove id from auditfield to maintain integrity and make avatar blank if none supplied --- apiserver/plane/api/views/integration/base.py | 10 +++++++++- apiserver/plane/db/mixins.py | 4 ---- apiserver/plane/db/models/integration/base.py | 5 +++++ apiserver/plane/db/models/integration/github.py | 6 ++++++ 4 files changed, 20 insertions(+), 5 deletions(-) diff --git a/apiserver/plane/api/views/integration/base.py b/apiserver/plane/api/views/integration/base.py index 001ced857aa..9a878768835 100644 --- a/apiserver/plane/api/views/integration/base.py +++ b/apiserver/plane/api/views/integration/base.py @@ -97,7 +97,9 @@ def create(self, request, slug): is_password_autoset=True, is_bot=True, first_name=integration.provider, - avatar=integration.avatar_url, + avatar=integration.avatar_url + if integration.avatar_url is not None + else "", ) # Create an API Token for the bot user @@ -130,6 +132,12 @@ def create(self, request, slug): {"error": "Integration is already active in the workspace"}, status=status.HTTP_410_GONE, ) + else: + capture_exception(e) + return Response( + {"error": "Something went wrong please try again later"}, + status=status.HTTP_400_BAD_REQUEST, + ) except (Workspace.DoesNotExist, Integration.DoesNotExist) as e: return Response( {"error": "Workspace or Integration not found"}, diff --git a/apiserver/plane/db/mixins.py b/apiserver/plane/db/mixins.py index 3b1b7fe87b1..728cb993351 100644 --- a/apiserver/plane/db/mixins.py +++ b/apiserver/plane/db/mixins.py @@ -46,9 +46,5 @@ class AuditModel(TimeAuditModel, UserAuditModel): """To path when the record was created and last modified""" - id = models.UUIDField( - default=uuid.uuid4, unique=True, editable=False, db_index=True, primary_key=True - ) - class Meta: abstract = True diff --git a/apiserver/plane/db/models/integration/base.py b/apiserver/plane/db/models/integration/base.py index d25a7477487..44f954cbef1 100644 --- a/apiserver/plane/db/models/integration/base.py +++ b/apiserver/plane/db/models/integration/base.py @@ -1,3 +1,6 @@ +# Python imports +import uuid + # Django imports from django.db import models @@ -7,6 +10,8 @@ class Integration(AuditModel): + + id = models.UUIDField(default=uuid.uuid4, unique=True, editable=False, db_index=True, primary_key=True) provider = models.CharField(max_length=400) network = models.PositiveIntegerField( default=1, choices=((1, "Private"), (2, "Public")) diff --git a/apiserver/plane/db/models/integration/github.py b/apiserver/plane/db/models/integration/github.py index 06362eee8fb..91f0763aa72 100644 --- a/apiserver/plane/db/models/integration/github.py +++ b/apiserver/plane/db/models/integration/github.py @@ -1,3 +1,6 @@ +# Python imports +import uuid + # Django imports from django.db import models @@ -7,6 +10,9 @@ class GithubRepository(AuditModel): + id = models.UUIDField( + default=uuid.uuid4, unique=True, editable=False, db_index=True, primary_key=True + ) name = models.CharField(max_length=500) url = models.URLField(null=True) config = models.JSONField(default=dict) From 01d5da53c36345016ad3ec3fd3b55eb3c64404f4 Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Sun, 12 Feb 2023 01:00:03 +0530 Subject: [PATCH 22/36] feat: track comments on an issue --- apiserver/plane/api/views/issue.py | 43 +++++- .../plane/bgtasks/issue_activites_task.py | 138 +++++++++++++----- 2 files changed, 139 insertions(+), 42 deletions(-) diff --git a/apiserver/plane/api/views/issue.py b/apiserver/plane/api/views/issue.py index acfce2409b2..e45b6c87fb4 100644 --- a/apiserver/plane/api/views/issue.py +++ b/apiserver/plane/api/views/issue.py @@ -3,7 +3,7 @@ from itertools import groupby, chain # Django imports -from django.db.models import Prefetch, OuterRef, Func, F +from django.db.models import Prefetch, OuterRef, Func, F, Q from django.core.serializers.json import DjangoJSONEncoder # Third Party imports @@ -77,7 +77,7 @@ def perform_update(self, serializer): if current_instance is not None: issue_activity.delay( { - "type": "issue.activity", + "type": "issue.activity.updated", "requested_data": requested_data, "actor_id": str(self.request.user.id), "issue_id": str(self.kwargs.get("pk", None)), @@ -202,7 +202,7 @@ def create(self, request, slug, project_id): # Track the issue issue_activity.delay( { - "type": "issue.activity", + "type": "issue.activity.created", "requested_data": json.dumps( self.request.data, cls=DjangoJSONEncoder ), @@ -306,7 +306,10 @@ def get(self, request, slug, project_id, issue_id): try: issue_activities = ( IssueActivity.objects.filter(issue_id=issue_id) - .filter(project__project_projectmember__member=self.request.user) + .filter( + ~Q(field="comment"), + project__project_projectmember__member=self.request.user, + ) .select_related("actor") ).order_by("created_by") issue_comments = ( @@ -349,6 +352,38 @@ def perform_create(self, serializer): issue_id=self.kwargs.get("issue_id"), actor=self.request.user if self.request.user is not None else None, ) + issue_activity.delay( + { + "type": "comment.activity.created", + "requested_data": json.dumps(serializer.data, cls=DjangoJSONEncoder), + "actor_id": str(self.request.user.id), + "issue_id": str(self.kwargs.get("issue_id")), + "project_id": str(self.kwargs.get("project_id")), + "current_instance": None, + }, + ) + + def perform_update(self, serializer): + requested_data = json.dumps(self.request.data, cls=DjangoJSONEncoder) + current_instance = ( + self.get_queryset().filter(pk=self.kwargs.get("pk", None)).first() + ) + if current_instance is not None: + issue_activity.delay( + { + "type": "comment.activity.updated", + "requested_data": requested_data, + "actor_id": str(self.request.user.id), + "issue_id": str(self.kwargs.get("issue_id", None)), + "project_id": str(self.kwargs.get("project_id", None)), + "current_instance": json.dumps( + IssueCommentSerializer(current_instance).data, + cls=DjangoJSONEncoder, + ), + }, + ) + + return super().perform_update(serializer) def get_queryset(self): return self.filter_queryset( diff --git a/apiserver/plane/bgtasks/issue_activites_task.py b/apiserver/plane/bgtasks/issue_activites_task.py index dfd32897dd3..5127773f6fe 100644 --- a/apiserver/plane/bgtasks/issue_activites_task.py +++ b/apiserver/plane/bgtasks/issue_activites_task.py @@ -618,6 +618,91 @@ def track_modules( ) +def create_issue_activity( + requested_data, current_instance, issue_id, project, actor, issue_activities +): + issue_activities.append( + IssueActivity( + issue_id=issue_id, + project=project, + workspace=project.workspace, + comment=f"{actor.email} created the issue", + verb="created", + actor=actor, + ) + ) + + +def update_issue_activity( + requested_data, current_instance, issue_id, project, actor, issue_activities +): + ISSUE_ACTIVITY_MAPPER = { + "name": track_name, + "parent": track_parent, + "priority": track_priority, + "state": track_state, + "description": track_description, + "target_date": track_target_date, + "start_date": track_start_date, + "labels_list": track_labels, + "assignees_list": track_assignees, + "blocks_list": track_blocks, + "blockers_list": track_blockings, + "cycles_list": track_cycles, + "modules_list": track_modules, + } + for key in requested_data: + func = ISSUE_ACTIVITY_MAPPER.get(key, None) + if func is not None: + func( + requested_data, + current_instance, + issue_id, + project, + actor, + issue_activities, + ) + + +def create_comment_activity( + requested_data, current_instance, issue_id, project, actor, issue_activities +): + issue_activities.append( + IssueActivity( + issue_id=issue_id, + project=project, + workspace=project.workspace, + comment=f"{actor.email} created a comment", + verb="created", + actor=actor, + field="comment", + new_value=requested_data.get("comment_html"), + new_identifier=requested_data.get("id"), + ) + ) + + +def update_comment_activity( + requested_data, current_instance, issue_id, project, actor, issue_activities +): + if current_instance.get("comment_html") != requested_data.get("comment_html"): + issue_activities.append( + IssueActivity( + issue_id=issue_id, + project=project, + workspace=project.workspace, + comment=f"{actor.email} updated a comment", + verb="updated", + actor=actor, + field="comment", + old_value=current_instance.get("comment_html"), + old_identifier=current_instance.get("id"), + new_value=requested_data.get("comment_html"), + new_identifier=requested_data.get("id"), + ) + ) + + # Receive message from room group @job("default") def issue_activity(event): @@ -638,46 +723,23 @@ def issue_activity(event): project = Project.objects.get(pk=project_id) - ISSUE_ACTIVITY_MAPPER = { - "name": track_name, - "parent": track_parent, - "priority": track_priority, - "state": track_state, - "description": track_description, - "target_date": track_target_date, - "start_date": track_start_date, - "labels_list": track_labels, - "assignees_list": track_assignees, - "blocks_list": track_blocks, - "blockers_list": track_blockings, - "cycles_list": track_cycles, - "modules_list": track_modules, + ACTIVITY_MAPPER = { + "issue.activity.created": create_issue_activity, + "issue.activity.updated": update_issue_activity, + "comment.activity.created": create_comment_activity, + "comment.activity.updated": update_comment_activity, } - if current_instance is None: - if type == "issue.activity": - issue_activities.append( - IssueActivity( - issue_id=issue_id, - project=project, - workspace=project.workspace, - comment=f"{actor.email} created the issue", - verb="created", - actor=actor, - ) - ) - else: - for key in requested_data: - func = ISSUE_ACTIVITY_MAPPER.get(key, None) - if func is not None: - func( - requested_data, - current_instance, - issue_id, - project, - actor, - issue_activities, - ) + func = ACTIVITY_MAPPER.get(type) + if func is not None: + func( + requested_data, + current_instance, + issue_id, + project, + actor, + issue_activities, + ) # Save all the values to database issue_activities_created = IssueActivity.objects.bulk_create(issue_activities) From e61ea48880090bc79b86521c4330a97fc6ebdfbd Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Sun, 12 Feb 2023 11:57:56 +0530 Subject: [PATCH 23/36] feat: comment syncing from plane to github --- apiserver/plane/api/serializers/__init__.py | 1 + .../api/serializers/integration/__init__.py | 1 + .../api/serializers/integration/github.py | 19 +++++++++++- apiserver/plane/api/urls.py | 30 ++++++++++++++++++- apiserver/plane/api/views/__init__.py | 1 + .../plane/api/views/integration/__init__.py | 1 + .../plane/api/views/integration/github.py | 13 ++++++++ .../plane/bgtasks/issue_activites_task.py | 4 ++- apiserver/plane/db/models/__init__.py | 1 + .../plane/db/models/integration/__init__.py | 2 +- .../plane/db/models/integration/github.py | 21 +++++++++++++ 11 files changed, 90 insertions(+), 4 deletions(-) diff --git a/apiserver/plane/api/serializers/__init__.py b/apiserver/plane/api/serializers/__init__.py index 2830536f5df..183129939f2 100644 --- a/apiserver/plane/api/serializers/__init__.py +++ b/apiserver/plane/api/serializers/__init__.py @@ -48,4 +48,5 @@ GithubIssueSyncSerializer, GithubRepositorySerializer, GithubRepositorySyncSerializer, + GithubCommentSyncSerializer, ) diff --git a/apiserver/plane/api/serializers/integration/__init__.py b/apiserver/plane/api/serializers/integration/__init__.py index 73173a30c78..8aea68bd6d5 100644 --- a/apiserver/plane/api/serializers/integration/__init__.py +++ b/apiserver/plane/api/serializers/integration/__init__.py @@ -3,4 +3,5 @@ GithubRepositorySerializer, GithubRepositorySyncSerializer, GithubIssueSyncSerializer, + GithubCommentSyncSerializer, ) diff --git a/apiserver/plane/api/serializers/integration/github.py b/apiserver/plane/api/serializers/integration/github.py index 66c310346dd..8352dcee143 100644 --- a/apiserver/plane/api/serializers/integration/github.py +++ b/apiserver/plane/api/serializers/integration/github.py @@ -1,6 +1,11 @@ # Module imports from plane.api.serializers import BaseSerializer -from plane.db.models import GithubIssueSync, GithubRepository, GithubRepositorySync +from plane.db.models import ( + GithubIssueSync, + GithubRepository, + GithubRepositorySync, + GithubCommentSync, +) class GithubRepositorySerializer(BaseSerializer): @@ -26,3 +31,15 @@ class Meta: "workspace", "repository_sync", ] + + +class GithubCommentSyncSerializer(BaseSerializer): + class Meta: + model = GithubCommentSync + fields = "__all__" + read_only_fields = [ + "project", + "workspace", + "repository_sync", + "issue_sync", + ] diff --git a/apiserver/plane/api/urls.py b/apiserver/plane/api/urls.py index 97b23e04d8f..b809c465242 100644 --- a/apiserver/plane/api/urls.py +++ b/apiserver/plane/api/urls.py @@ -92,6 +92,7 @@ WorkspaceIntegrationViewSet, GithubRepositorySyncViewSet, GithubIssueSyncViewSet, + GithubCommentSyncViewSet, ## End Integrations ) @@ -744,6 +745,15 @@ } ), ), + path( + "workspaces//projects//workspace-integrations//github-repository-sync//", + GithubRepositorySyncViewSet.as_view( + { + "get": "retrieve", + "delete": "destroy", + } + ), + ), path( "workspaces//projects//github-repository-sync//github-issue-sync/", GithubIssueSyncViewSet.as_view( @@ -755,7 +765,25 @@ ), path( "workspaces//projects//github-repository-sync//github-issue-sync//", - GithubRepositorySyncViewSet.as_view( + GithubIssueSyncViewSet.as_view( + { + "get": "retrieve", + "delete": "destroy", + } + ), + ), + path( + "workspaces//projects//github-repository-sync//github-issue-sync//github-comment-sync/", + GithubCommentSyncViewSet.as_view( + { + "post": "create", + "get": "list", + } + ), + ), + path( + "workspaces//projects//github-repository-sync//github-issue-sync//github-comment-sync//", + GithubCommentSyncViewSet.as_view( { "get": "retrieve", "delete": "destroy", diff --git a/apiserver/plane/api/views/__init__.py b/apiserver/plane/api/views/__init__.py index c21399bb449..d070d55c0e2 100644 --- a/apiserver/plane/api/views/__init__.py +++ b/apiserver/plane/api/views/__init__.py @@ -80,4 +80,5 @@ IntegrationViewSet, GithubIssueSyncViewSet, GithubRepositorySyncViewSet, + GithubCommentSyncViewSet, ) diff --git a/apiserver/plane/api/views/integration/__init__.py b/apiserver/plane/api/views/integration/__init__.py index 29d8d1ffec9..095e9d1f7f9 100644 --- a/apiserver/plane/api/views/integration/__init__.py +++ b/apiserver/plane/api/views/integration/__init__.py @@ -2,4 +2,5 @@ from .github import ( GithubRepositorySyncViewSet, GithubIssueSyncViewSet, + GithubCommentSyncViewSet, ) diff --git a/apiserver/plane/api/views/integration/github.py b/apiserver/plane/api/views/integration/github.py index a95d055d5d7..e69029f6277 100644 --- a/apiserver/plane/api/views/integration/github.py +++ b/apiserver/plane/api/views/integration/github.py @@ -12,11 +12,13 @@ WorkspaceIntegration, ProjectMember, Label, + GithubCommentSync, ) from plane.api.serializers import ( GithubRepositorySerializer, GithubIssueSyncSerializer, GithubRepositorySyncSerializer, + GithubCommentSyncSerializer, ) @@ -112,3 +114,14 @@ def perform_create(self, serializer): project_id=self.kwargs.get("project_id"), repository_sync_id=self.kwargs.get("repo_sync_id"), ) + + +class GithubCommentSyncViewSet(BaseViewSet): + serializer_class = GithubCommentSyncSerializer + model = GithubCommentSync + + def perform_create(self, serializer): + serializer.save( + project_id=self.kwargs.get("project_id"), + issue_sync_id=self.kwargs.get("issue_sync_id"), + ) diff --git a/apiserver/plane/bgtasks/issue_activites_task.py b/apiserver/plane/bgtasks/issue_activites_task.py index 5127773f6fe..f2f46c4bacc 100644 --- a/apiserver/plane/bgtasks/issue_activites_task.py +++ b/apiserver/plane/bgtasks/issue_activites_task.py @@ -678,6 +678,7 @@ def create_comment_activity( field="comment", new_value=requested_data.get("comment_html"), new_identifier=requested_data.get("id"), + issue_comment_id=requested_data.get("id", None), ) ) @@ -698,7 +699,8 @@ def update_comment_activity( old_value=current_instance.get("comment_html"), old_identifier=current_instance.get("id"), new_value=requested_data.get("comment_html"), - new_identifier=requested_data.get("id"), + new_identifier=current_instance.get("id"), + issue_comment_id=current_instance.get("id"), ) ) diff --git a/apiserver/plane/db/models/__init__.py b/apiserver/plane/db/models/__init__.py index 6c9ccf58125..4613c830cd7 100644 --- a/apiserver/plane/db/models/__init__.py +++ b/apiserver/plane/db/models/__init__.py @@ -53,4 +53,5 @@ GithubRepository, GithubRepositorySync, GithubIssueSync, + GithubCommentSync, ) diff --git a/apiserver/plane/db/models/integration/__init__.py b/apiserver/plane/db/models/integration/__init__.py index 74265a6f469..4742a25293a 100644 --- a/apiserver/plane/db/models/integration/__init__.py +++ b/apiserver/plane/db/models/integration/__init__.py @@ -1,2 +1,2 @@ from .base import Integration, WorkspaceIntegration -from .github import GithubRepository, GithubRepositorySync, GithubIssueSync +from .github import GithubRepository, GithubRepositorySync, GithubIssueSync, GithubCommentSync diff --git a/apiserver/plane/db/models/integration/github.py b/apiserver/plane/db/models/integration/github.py index 91f0763aa72..265168cafbb 100644 --- a/apiserver/plane/db/models/integration/github.py +++ b/apiserver/plane/db/models/integration/github.py @@ -78,3 +78,24 @@ class Meta: verbose_name_plural = "Github Issue Syncs" db_table = "github_issue_syncs" ordering = ("-created_at",) + + +class GithubCommentSync(ProjectBaseModel): + repo_comment_id = models.BigIntegerField() + comment = models.ForeignKey( + "db.IssueComment", related_name="comment_syncs", on_delete=models.CASCADE + ) + issue_sync = models.ForeignKey( + "db.GithubIssueSync", related_name="comment_syncs", on_delete=models.CASCADE + ) + + def __str__(self): + """Return the github issue sync""" + return f"{self.comment.id}" + + class Meta: + unique_together = ["issue_sync", "comment"] + verbose_name = "Github Comment Sync" + verbose_name_plural = "Github Comment Syncs" + db_table = "github_comment_syncs" + ordering = ("-created_at",) From e685175711142cfee9416fa0851964152e52e590 Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Sun, 12 Feb 2023 14:23:34 +0530 Subject: [PATCH 24/36] fix: prevent activities created by bot to be sent to webhook --- .../plane/bgtasks/issue_activites_task.py | 25 +++++++++++-------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/apiserver/plane/bgtasks/issue_activites_task.py b/apiserver/plane/bgtasks/issue_activites_task.py index f2f46c4bacc..11b70c6f7ce 100644 --- a/apiserver/plane/bgtasks/issue_activites_task.py +++ b/apiserver/plane/bgtasks/issue_activites_task.py @@ -746,17 +746,20 @@ def issue_activity(event): # Save all the values to database issue_activities_created = IssueActivity.objects.bulk_create(issue_activities) # Post the updates to segway for integrations and webhooks - if settings.PROXY_BASE_URL: - for issue_activity in issue_activities_created: - headers = {"Content-Type": "application/json"} - issue_activity_json = json.dumps( - IssueActivitySerializer(issue_activity).data, cls=DjangoJSONEncoder - ) - _ = requests.post( - f"{settings.PROXY_BASE_URL}/hooks/workspaces/{str(issue_activity.workspace_id)}/projects/{str(issue_activity.project_id)}/issues/{str(issue_activity.issue_id)}/issue-activity-hooks/", - json=issue_activity_json, - headers=headers, - ) + if len(issue_activities_created): + # Don't send activities if the actor is a bot + if settings.PROXY_BASE_URL and not actor.is_bot: + for issue_activity in issue_activities_created: + headers = {"Content-Type": "application/json"} + issue_activity_json = json.dumps( + IssueActivitySerializer(issue_activity).data, + cls=DjangoJSONEncoder, + ) + _ = requests.post( + f"{settings.PROXY_BASE_URL}/hooks/workspaces/{str(issue_activity.workspace_id)}/projects/{str(issue_activity.project_id)}/issues/{str(issue_activity.issue_id)}/issue-activity-hooks/", + json=issue_activity_json, + headers=headers, + ) return except Exception as e: capture_exception(e) From 1dc717ec25e1dd1a34d89d30b2c6656d39a5658d Mon Sep 17 00:00:00 2001 From: venplane Date: Mon, 13 Feb 2023 10:43:13 +0530 Subject: [PATCH 25/36] feat: github app installation id retrieve --- apiserver/plane/api/urls.py | 5 ++ apiserver/plane/api/views/__init__.py | 1 + .../plane/api/views/integration/__init__.py | 1 + .../plane/api/views/integration/github.py | 64 +++++++++++++++++++ apps/app/layouts/app-layout/index.tsx | 4 ++ 5 files changed, 75 insertions(+) diff --git a/apiserver/plane/api/urls.py b/apiserver/plane/api/urls.py index d4f68f66c4f..eb187ad835a 100644 --- a/apiserver/plane/api/urls.py +++ b/apiserver/plane/api/urls.py @@ -92,6 +92,7 @@ WorkspaceIntegrationViewSet, GithubRepositorySyncViewSet, GithubIssueSyncViewSet, + GithubAppInstallationViewSet, ## End Integrations ) @@ -762,6 +763,10 @@ } ), ), + path( + "workspaces//github-installations//", + GithubAppInstallationViewSet.as_view(), + ) ## End Github Integrations ## End Integrations ] diff --git a/apiserver/plane/api/views/__init__.py b/apiserver/plane/api/views/__init__.py index c21399bb449..ef19f9dfb6f 100644 --- a/apiserver/plane/api/views/__init__.py +++ b/apiserver/plane/api/views/__init__.py @@ -80,4 +80,5 @@ IntegrationViewSet, GithubIssueSyncViewSet, GithubRepositorySyncViewSet, + GithubAppInstallationViewSet, ) diff --git a/apiserver/plane/api/views/integration/__init__.py b/apiserver/plane/api/views/integration/__init__.py index 29d8d1ffec9..c714e56df67 100644 --- a/apiserver/plane/api/views/integration/__init__.py +++ b/apiserver/plane/api/views/integration/__init__.py @@ -2,4 +2,5 @@ from .github import ( GithubRepositorySyncViewSet, GithubIssueSyncViewSet, + GithubAppInstallationViewSet, ) diff --git a/apiserver/plane/api/views/integration/github.py b/apiserver/plane/api/views/integration/github.py index 98f23c9ff75..123e998de94 100644 --- a/apiserver/plane/api/views/integration/github.py +++ b/apiserver/plane/api/views/integration/github.py @@ -1,6 +1,7 @@ # Third party imports from rest_framework import status from rest_framework.response import Response +from rest_framework.views import APIView from sentry_sdk import capture_exception # Module imports @@ -89,3 +90,66 @@ class GithubIssueSyncViewSet(BaseViewSet): def perform_create(self, serializer): serializer.save(project_id=self.kwargs.get("project_id")) + + +class GithubAppInstallationViewSet(APIView): + def get_jwt_token(self): + import jwt + from datetime import timedelta, now + from cryptography.hazmat.primitives.serialization import load_pem_private_key + from cryptography.hazmat.backends import default_backend + + app_id = "291184" + secret = b"""-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAv6muOm754ZqIbUkGngGuRNFSDnSuROMN8aIjjgMa2HR+ts1c +nItXWiSvlPJaNCLmmjduTnh7KaxvPzQwCFAK61P/7ZfRNPqzaMibPeolDSKNZ+bF +lHcB/HA+XQN/OJ3UvvK9O0bnjYcpmjJJ61rV7fC+gSzEtG4UrPMihK+koKNlyA11 +gehQkX/MGdCFEX94Hw1a+B6g2eXF1QRYkVh/Q0JLyHcV/TmSwPNi171V2Kn/ClRq +8xyZFE7EpzEf/hCP3TFvQD+iVfKnANdXC0nGG4k7NKvYnitI/hb5acpKr5khRap9 +WEEvup03waTqUDqsiYJnx+pX60VQs/ooqns0AQIDAQABAoIBAFpL+sWU2y0qvPOf +3/o5GH1bkKk215Ok1UDt/oo9dDxeRgSho+wsya6ycfZeZ5kAiFxHmTRnFr9/ebnx +QF/qNfrAzGaHjAzFwBixylHVKjeR+8TjuFpF5y2jQ/5WU229DioX+oce1KCc+UCo +SKMVXIyJS3dYPCQ1aJBCzBRvG8SB5Z9rCSN79m8QS6ZXHsiYUQhZhi0ExYSdl8tn +hbbf+dzQ5DyDRmf+2vwSg1M+/uiEwb478pImFHpk34Yyl4r7etXm044DRldUWHr+ +RZnWlXQXFFL1QIfhBQbHsAgUxAjjhH5I6ATEuJLySBVJDF+PhzowjWhqAQEp8vy6 ++ogiMXECgYEA7Drjn4tfR25SGzTUmSI7HmmEoFwwRcS/nyO/igB4CZ/ODPlBe8jx +p3HrpCwEfu7TooUhkso2REvabyTTjlRl2B0Twi9mStr7UHKxJEUdeZA99YxzTjfa +DBHGBx7f/1hYJdL/GKoKb/Eh9dlx9gg8PwfotZ/lQg5P0B/MsKF+ZscCgYEAz7P1 +/pG0bsNGrMKaLN+4WQpQyr1FQo/u1CL/N7ZF1qwnU8IMBcOb97AoatC2/2DvctDE +dwCZVWjDLy2lDuFkXCMy7DRx1MF8VLyXJzLaWrdun1nwR0w0JRRGJQDzaee5U4Td +jX4eXKBuy6T/Xnhh3cFYXUd/jTsF++WdRiEXpvcCgYBy87keQwDrTojPymaF2f1w +sCIkspee673fX9LuAYpoDIaFE6nE5aSKOcpUCkNpzSfZFvWea536n/q8SOxVf0ZL +4uJhhRU+6c6PeDAxGRzdsc4kteLKNi154BBAGMshg0jppwIRa3VGwc0nyFdHRPyk +I2IfN56lBTfbbA38CanrswKBgAoNh8Z+fuEtimoqMRQi7+U/XpGxf3ytQr35w6iK +pe6x/mVLaxGMWiwu1oX0/CZ4Jp7EA/5OhR1hKLFL4EVMG3NqMLjGAQxvIPlo91fq +Wi8x2aTU0ZBh29Q/mvWHikCB+rJUJ/UFOar6COLKZaHI6dO12/UH1OCdDrkWb/pI +98AZAoGBAKQkQYFrjbzcfCAYiaeWLE5k2zHT37qmLdq6CM5YJCicxhfFTYVCkiET +1YJHUgk5UWAlSNcF/fT7+LBT9cSSxNoZIl5TDLpwmYaNhgTm1R7yecGE27Q4U4Mo +EzNTJHn4jU5S8WGOjmctIaBwHNSn83n6yRdsQh7yOBztnwIBNMX8 +-----END RSA PRIVATE KEY----- +""" + + due_date = now() + timedelta(minutes=10) + expiry = int(due_date.timestamp()) + payload = { + "iss": app_id, + "sub": app_id, + "exp": expiry, + "aud": "https://github.com/login/oauth/access_token", + } + + priv_rsakey = load_pem_private_key(secret, None, default_backend()) + token = jwt.encode(payload, priv_rsakey, algorithm="RS256") + return token + + def post(self, request, installation_id): + token = self.get_jwt_token() + import requests + + url = f"https://api.github.com/app/installations/{installation_id}" + headers = { + "Authorization": "Bearer " + token, + "Accept": "application/vnd.github+json", + } + response = requests.get(url, headers=headers).json() + return Response(response) diff --git a/apps/app/layouts/app-layout/index.tsx b/apps/app/layouts/app-layout/index.tsx index d6462fdd374..c03c2b39d70 100644 --- a/apps/app/layouts/app-layout/index.tsx +++ b/apps/app/layouts/app-layout/index.tsx @@ -61,6 +61,10 @@ const workspaceLinks: (wSlug: string) => Array<{ label: "Billing & Plans", href: `/${workspaceSlug}/settings/billing`, }, + { + label: "Integrations", + href: `/${workspaceSlug}/settings/integrations`, + }, ]; const sidebarLinks: ( From 2adcb7e8311e654cd5a2481c31cb5404115872c7 Mon Sep 17 00:00:00 2001 From: venplane Date: Mon, 13 Feb 2023 14:03:53 +0530 Subject: [PATCH 26/36] feat: github app installation id saved into db --- .../plane/api/views/integration/github.py | 68 +++++++-------- apps/app/components/popup/index.tsx | 41 +++++++++ .../[workspaceSlug]/settings/integrations.tsx | 84 +++++++++++++++++++ apps/app/pages/installations/github/index.tsx | 38 +++++++++ 4 files changed, 193 insertions(+), 38 deletions(-) create mode 100644 apps/app/components/popup/index.tsx create mode 100644 apps/app/pages/[workspaceSlug]/settings/integrations.tsx create mode 100644 apps/app/pages/installations/github/index.tsx diff --git a/apiserver/plane/api/views/integration/github.py b/apiserver/plane/api/views/integration/github.py index 481630dff99..ce6398da7fa 100644 --- a/apiserver/plane/api/views/integration/github.py +++ b/apiserver/plane/api/views/integration/github.py @@ -1,7 +1,14 @@ +import os +import jwt +from datetime import timedelta, datetime +from cryptography.hazmat.primitives.serialization import load_pem_private_key +from cryptography.hazmat.backends import default_backend + # Third party imports from rest_framework import status from rest_framework.response import Response from rest_framework.views import APIView +from rest_framework.permissions import AllowAny from sentry_sdk import capture_exception # Module imports @@ -129,48 +136,19 @@ def perform_create(self, serializer): class GithubAppInstallationViewSet(APIView): + permission_classes = [AllowAny] + def get_jwt_token(self): - import jwt - from datetime import timedelta, now - from cryptography.hazmat.primitives.serialization import load_pem_private_key - from cryptography.hazmat.backends import default_backend - - app_id = "291184" - secret = b"""-----BEGIN RSA PRIVATE KEY----- -MIIEowIBAAKCAQEAv6muOm754ZqIbUkGngGuRNFSDnSuROMN8aIjjgMa2HR+ts1c -nItXWiSvlPJaNCLmmjduTnh7KaxvPzQwCFAK61P/7ZfRNPqzaMibPeolDSKNZ+bF -lHcB/HA+XQN/OJ3UvvK9O0bnjYcpmjJJ61rV7fC+gSzEtG4UrPMihK+koKNlyA11 -gehQkX/MGdCFEX94Hw1a+B6g2eXF1QRYkVh/Q0JLyHcV/TmSwPNi171V2Kn/ClRq -8xyZFE7EpzEf/hCP3TFvQD+iVfKnANdXC0nGG4k7NKvYnitI/hb5acpKr5khRap9 -WEEvup03waTqUDqsiYJnx+pX60VQs/ooqns0AQIDAQABAoIBAFpL+sWU2y0qvPOf -3/o5GH1bkKk215Ok1UDt/oo9dDxeRgSho+wsya6ycfZeZ5kAiFxHmTRnFr9/ebnx -QF/qNfrAzGaHjAzFwBixylHVKjeR+8TjuFpF5y2jQ/5WU229DioX+oce1KCc+UCo -SKMVXIyJS3dYPCQ1aJBCzBRvG8SB5Z9rCSN79m8QS6ZXHsiYUQhZhi0ExYSdl8tn -hbbf+dzQ5DyDRmf+2vwSg1M+/uiEwb478pImFHpk34Yyl4r7etXm044DRldUWHr+ -RZnWlXQXFFL1QIfhBQbHsAgUxAjjhH5I6ATEuJLySBVJDF+PhzowjWhqAQEp8vy6 -+ogiMXECgYEA7Drjn4tfR25SGzTUmSI7HmmEoFwwRcS/nyO/igB4CZ/ODPlBe8jx -p3HrpCwEfu7TooUhkso2REvabyTTjlRl2B0Twi9mStr7UHKxJEUdeZA99YxzTjfa -DBHGBx7f/1hYJdL/GKoKb/Eh9dlx9gg8PwfotZ/lQg5P0B/MsKF+ZscCgYEAz7P1 -/pG0bsNGrMKaLN+4WQpQyr1FQo/u1CL/N7ZF1qwnU8IMBcOb97AoatC2/2DvctDE -dwCZVWjDLy2lDuFkXCMy7DRx1MF8VLyXJzLaWrdun1nwR0w0JRRGJQDzaee5U4Td -jX4eXKBuy6T/Xnhh3cFYXUd/jTsF++WdRiEXpvcCgYBy87keQwDrTojPymaF2f1w -sCIkspee673fX9LuAYpoDIaFE6nE5aSKOcpUCkNpzSfZFvWea536n/q8SOxVf0ZL -4uJhhRU+6c6PeDAxGRzdsc4kteLKNi154BBAGMshg0jppwIRa3VGwc0nyFdHRPyk -I2IfN56lBTfbbA38CanrswKBgAoNh8Z+fuEtimoqMRQi7+U/XpGxf3ytQr35w6iK -pe6x/mVLaxGMWiwu1oX0/CZ4Jp7EA/5OhR1hKLFL4EVMG3NqMLjGAQxvIPlo91fq -Wi8x2aTU0ZBh29Q/mvWHikCB+rJUJ/UFOar6COLKZaHI6dO12/UH1OCdDrkWb/pI -98AZAoGBAKQkQYFrjbzcfCAYiaeWLE5k2zHT37qmLdq6CM5YJCicxhfFTYVCkiET -1YJHUgk5UWAlSNcF/fT7+LBT9cSSxNoZIl5TDLpwmYaNhgTm1R7yecGE27Q4U4Mo -EzNTJHn4jU5S8WGOjmctIaBwHNSn83n6yRdsQh7yOBztnwIBNMX8 ------END RSA PRIVATE KEY----- -""" - - due_date = now() + timedelta(minutes=10) + app_id = os.environ.get("GITHUB_APP_ID", "") + secret = bytes(os.environ.get("GITHUB_APP_PRIVATE_KEY", ""), encoding="utf8") + current_timestamp = int(datetime.now().timestamp()) + due_date = datetime.now() + timedelta(minutes=10) expiry = int(due_date.timestamp()) payload = { "iss": app_id, "sub": app_id, "exp": expiry, + "iat": current_timestamp, "aud": "https://github.com/login/oauth/access_token", } @@ -178,7 +156,7 @@ def get_jwt_token(self): token = jwt.encode(payload, priv_rsakey, algorithm="RS256") return token - def post(self, request, installation_id): + def post(self, request, slug, installation_id): token = self.get_jwt_token() import requests @@ -188,4 +166,18 @@ def post(self, request, installation_id): "Accept": "application/vnd.github+json", } response = requests.get(url, headers=headers).json() - return Response(response) + + # serializer = GithubRepositorySerializer( + # data={ + # "name": response.get("app_slug"), + # "url": response.get("html_url"), + # "repository_id": response.get("id"), + # "config": response, + # "owner": response.get("account").get("login"), + # } + # ) + + # if serializer.is_valid(raise_exception=True): + # serializer.save() + + return Response("Created") diff --git a/apps/app/components/popup/index.tsx b/apps/app/components/popup/index.tsx new file mode 100644 index 00000000000..439aab62aaf --- /dev/null +++ b/apps/app/components/popup/index.tsx @@ -0,0 +1,41 @@ +import { useRouter } from "next/router"; +import React, { useRef } from "react"; + +const OAuthPopUp = ({ workspaceSlug }: any) => { + const popup = useRef(); + + const router = useRouter(); + + const checkPopup = () => { + const check = setInterval(() => { + if (!popup || popup.current.closed || popup.current.closed === undefined) { + clearInterval(check); + } + }, 1000); + }; + + const openPopup = () => { + const width = 600, + height = 600; + const left = window.innerWidth / 2 - width / 2; + const top = window.innerHeight / 2 - height / 2; + const url = `https://github.com/apps/plane-web-local/installations/new?state=${workspaceSlug}`; + + return window.open(url, "", `width=${width}, height=${height}, top=${top}, left=${left}`); + }; + + const startAuth = () => { + popup.current = openPopup(); + checkPopup(); + }; + + return ( + <> +
+ +
+ + ); +}; + +export default OAuthPopUp; diff --git a/apps/app/pages/[workspaceSlug]/settings/integrations.tsx b/apps/app/pages/[workspaceSlug]/settings/integrations.tsx new file mode 100644 index 00000000000..d8cb88946d8 --- /dev/null +++ b/apps/app/pages/[workspaceSlug]/settings/integrations.tsx @@ -0,0 +1,84 @@ +import React from "react"; + +import { useRouter } from "next/router"; +import useSWR from "swr"; + +// lib +import type { NextPage, GetServerSideProps } from "next"; +import { requiredWorkspaceAdmin } from "lib/auth"; +// constants +// services +import workspaceService from "services/workspace.service"; +// layouts +import AppLayout from "layouts/app-layout"; +// ui +import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs"; +import { WORKSPACE_DETAILS } from "constants/fetch-keys"; +import OAuthPopUp from "components/popup"; + +type TWorkspaceIntegrationsProps = { + isOwner: boolean; + isMember: boolean; + isViewer: boolean; + isGuest: boolean; +}; + +const WorkspaceIntegrations: NextPage = (props) => { + const { + query: { workspaceSlug }, + } = useRouter(); + + const { data: activeWorkspace } = useSWR( + workspaceSlug ? WORKSPACE_DETAILS(workspaceSlug as string) : null, + () => (workspaceSlug ? workspaceService.getWorkspace(workspaceSlug as string) : null) + ); + + return ( + <> + + + + + } + > +
+ {/* */} + +
+
+ + ); +}; + +export const getServerSideProps: GetServerSideProps = async (ctx) => { + const workspaceSlug = ctx.params?.workspaceSlug as string; + + const memberDetail = await requiredWorkspaceAdmin(workspaceSlug, ctx.req.headers.cookie); + + if (memberDetail === null) { + return { + redirect: { + destination: "/", + permanent: false, + }, + }; + } + + return { + props: { + isOwner: memberDetail?.role === 20, + isMember: memberDetail?.role === 15, + isViewer: memberDetail?.role === 10, + isGuest: memberDetail?.role === 5, + }, + }; +}; + +export default WorkspaceIntegrations; diff --git a/apps/app/pages/installations/github/index.tsx b/apps/app/pages/installations/github/index.tsx new file mode 100644 index 00000000000..325b424d6e2 --- /dev/null +++ b/apps/app/pages/installations/github/index.tsx @@ -0,0 +1,38 @@ +import React, { useEffect } from "react"; +import appinstallationsService from "services/appinstallations.service"; + +interface IGithuPostInstallationProps { + installation_id: string; + setup_action: string; + state: string; +} + +const AppPostInstallation = ({ + installation_id, + setup_action, + state, +}: IGithuPostInstallationProps) => { + useEffect(() => { + if (state && installation_id) { + appinstallationsService + .addGithubApp(state, installation_id) + .then((res) => { + window.opener = null; + window.open("", "_self"); + window.close(); + }) + .catch((err) => { + console.log(err); + }); + } + }, [state, installation_id]); + return <>Loading...; +}; + +export async function getServerSideProps(context: any) { + return { + props: context.query, + }; +} + +export default AppPostInstallation; From 1f8dbe636d45e249c5b713a9e7bbb0ae26b13f9c Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Mon, 13 Feb 2023 20:10:26 +0530 Subject: [PATCH 27/36] feat: installation_id for the github integragation and unique provider and project base integration for repo --- .../plane/api/views/integration/github.py | 18 ++++++++++++++---- apiserver/plane/db/models/integration/base.py | 7 ++++--- .../plane/db/models/integration/github.py | 7 +++---- 3 files changed, 21 insertions(+), 11 deletions(-) diff --git a/apiserver/plane/api/views/integration/github.py b/apiserver/plane/api/views/integration/github.py index e69029f6277..dada4ed1630 100644 --- a/apiserver/plane/api/views/integration/github.py +++ b/apiserver/plane/api/views/integration/github.py @@ -15,7 +15,6 @@ GithubCommentSync, ) from plane.api.serializers import ( - GithubRepositorySerializer, GithubIssueSyncSerializer, GithubRepositorySyncSerializer, GithubCommentSyncSerializer, @@ -36,10 +35,19 @@ def create(self, request, slug, project_id, workspace_integration_id): config = request.data.get("config", {}) repository_id = request.data.get("repository_id", False) owner = request.data.get("owner", False) - - if not name or not url or not repository_id or not owner: + installation_id = request.data.get("installation_id", False) + + if ( + not name + or not url + or not repository_id + or not owner + or not installation_id + ): return Response( - {"error": "Name, url, and repository_id are required"}, + { + "error": "Name, url, repository_id, owner and installation_id are required" + }, status=status.HTTP_400_BAD_REQUEST, ) @@ -50,6 +58,7 @@ def create(self, request, slug, project_id, workspace_integration_id): config=config, repository_id=repository_id, owner=owner, + project_id=project_id, ) # Get the workspace integration @@ -79,6 +88,7 @@ def create(self, request, slug, project_id, workspace_integration_id): credentials=request.data.get("credentials", {}), project_id=project_id, label=label, + installation_id=installation_id, ) # Add bot as a member in the project diff --git a/apiserver/plane/db/models/integration/base.py b/apiserver/plane/db/models/integration/base.py index 44f954cbef1..8eaa70809ec 100644 --- a/apiserver/plane/db/models/integration/base.py +++ b/apiserver/plane/db/models/integration/base.py @@ -10,9 +10,10 @@ class Integration(AuditModel): - - id = models.UUIDField(default=uuid.uuid4, unique=True, editable=False, db_index=True, primary_key=True) - provider = models.CharField(max_length=400) + id = models.UUIDField( + default=uuid.uuid4, unique=True, editable=False, db_index=True, primary_key=True + ) + provider = models.CharField(max_length=400, unique=True) network = models.PositiveIntegerField( default=1, choices=((1, "Private"), (2, "Public")) ) diff --git a/apiserver/plane/db/models/integration/github.py b/apiserver/plane/db/models/integration/github.py index 265168cafbb..07935056d1f 100644 --- a/apiserver/plane/db/models/integration/github.py +++ b/apiserver/plane/db/models/integration/github.py @@ -9,10 +9,7 @@ from plane.db.mixins import AuditModel -class GithubRepository(AuditModel): - id = models.UUIDField( - default=uuid.uuid4, unique=True, editable=False, db_index=True, primary_key=True - ) +class GithubRepository(ProjectBaseModel): name = models.CharField(max_length=500) url = models.URLField(null=True) config = models.JSONField(default=dict) @@ -31,6 +28,7 @@ class Meta: class GithubRepositorySync(ProjectBaseModel): + installation_id = models.BigIntegerField() repository = models.OneToOneField( "db.GithubRepository", on_delete=models.CASCADE, related_name="syncs" ) @@ -61,6 +59,7 @@ class Meta: class GithubIssueSync(ProjectBaseModel): repo_issue_id = models.BigIntegerField() github_issue_id = models.BigIntegerField() + issue_url = models.URLField(blank=False) issue = models.ForeignKey( "db.Issue", related_name="github_syncs", on_delete=models.CASCADE ) From 5cebce2e7a0d8ec76d66710aaa1384dec4c14d05 Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Mon, 13 Feb 2023 20:10:50 +0530 Subject: [PATCH 28/36] refactor: remove actor logic from activity task --- apiserver/plane/bgtasks/issue_activites_task.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apiserver/plane/bgtasks/issue_activites_task.py b/apiserver/plane/bgtasks/issue_activites_task.py index 11b70c6f7ce..9cdc4254727 100644 --- a/apiserver/plane/bgtasks/issue_activites_task.py +++ b/apiserver/plane/bgtasks/issue_activites_task.py @@ -748,7 +748,7 @@ def issue_activity(event): # Post the updates to segway for integrations and webhooks if len(issue_activities_created): # Don't send activities if the actor is a bot - if settings.PROXY_BASE_URL and not actor.is_bot: + if settings.PROXY_BASE_URL: for issue_activity in issue_activities_created: headers = {"Content-Type": "application/json"} issue_activity_json = json.dumps( From 0bf3e2966d8eb2b5a2fb7269d4091c426ddef3c0 Mon Sep 17 00:00:00 2001 From: venplane Date: Wed, 15 Feb 2023 01:36:18 +0530 Subject: [PATCH 29/36] feat: saving github metadata using installation id in workspace integration table --- apiserver/plane/api/urls.py | 7 +-- apiserver/plane/api/views/__init__.py | 1 - .../plane/api/views/integration/__init__.py | 1 - apiserver/plane/api/views/integration/base.py | 18 +++++-- .../plane/api/views/integration/github.py | 48 ------------------- apiserver/plane/db/models/integration/base.py | 3 ++ apiserver/plane/utils/integrations/github.py | 37 ++++++++++++++ apps/app/components/popup/index.tsx | 4 +- apps/app/constants/fetch-keys.ts | 1 + .../[workspaceSlug]/settings/integrations.tsx | 15 ++++-- .../{github => [provider]}/index.tsx | 7 ++- apps/app/services/appinstallations.service.ts | 4 +- apps/app/services/workspace.service.ts | 7 +++ 13 files changed, 83 insertions(+), 70 deletions(-) create mode 100644 apiserver/plane/utils/integrations/github.py rename apps/app/pages/installations/{github => [provider]}/index.tsx (82%) diff --git a/apiserver/plane/api/urls.py b/apiserver/plane/api/urls.py index 52251d80404..354740cd274 100644 --- a/apiserver/plane/api/urls.py +++ b/apiserver/plane/api/urls.py @@ -91,7 +91,6 @@ WorkspaceIntegrationViewSet, GithubRepositorySyncViewSet, GithubIssueSyncViewSet, - GithubAppInstallationViewSet, GithubCommentSyncViewSet, ## End Integrations ) @@ -715,7 +714,7 @@ name="integrations", ), path( - "workspaces//workspace-integrations/", + "workspaces//workspace-integrations//", WorkspaceIntegrationViewSet.as_view( { "get": "list", @@ -789,10 +788,6 @@ } ), ), - path( - "workspaces//github-installations//", - GithubAppInstallationViewSet.as_view(), - ) ## End Github Integrations ## End Integrations ] diff --git a/apiserver/plane/api/views/__init__.py b/apiserver/plane/api/views/__init__.py index 7d137464523..62e050ec31e 100644 --- a/apiserver/plane/api/views/__init__.py +++ b/apiserver/plane/api/views/__init__.py @@ -79,6 +79,5 @@ IntegrationViewSet, GithubIssueSyncViewSet, GithubRepositorySyncViewSet, - GithubAppInstallationViewSet, GithubCommentSyncViewSet, ) diff --git a/apiserver/plane/api/views/integration/__init__.py b/apiserver/plane/api/views/integration/__init__.py index 4f1871c75eb..095e9d1f7f9 100644 --- a/apiserver/plane/api/views/integration/__init__.py +++ b/apiserver/plane/api/views/integration/__init__.py @@ -2,6 +2,5 @@ from .github import ( GithubRepositorySyncViewSet, GithubIssueSyncViewSet, - GithubAppInstallationViewSet, GithubCommentSyncViewSet, ) diff --git a/apiserver/plane/api/views/integration/base.py b/apiserver/plane/api/views/integration/base.py index 9a878768835..79a6d089d17 100644 --- a/apiserver/plane/api/views/integration/base.py +++ b/apiserver/plane/api/views/integration/base.py @@ -21,6 +21,7 @@ APIToken, ) from plane.api.serializers import IntegrationSerializer, WorkspaceIntegrationSerializer +from plane.utils.integrations.github import get_github_metadata class IntegrationViewSet(BaseViewSet): @@ -76,18 +77,22 @@ class WorkspaceIntegrationViewSet(BaseViewSet): serializer_class = WorkspaceIntegrationSerializer model = WorkspaceIntegration - def create(self, request, slug): + def create(self, request, slug, provider): try: - integration = request.data.get("integration", False) + installation_id = request.data.get("installation_id", None) - if not integration: + if not installation_id: return Response( - {"error": "Integration is required"}, + {"error": "Installation ID is required"}, status=status.HTTP_400_BAD_REQUEST, ) workspace = Workspace.objects.get(slug=slug) - integration = Integration.objects.get(pk=integration) + integration = Integration.objects.get(provider=provider) + config = {} + if provider == "github": + metadata = get_github_metadata(installation_id) + config = {"installation_id": installation_id} # Create a bot user bot_user = User.objects.create( @@ -114,6 +119,8 @@ def create(self, request, slug): integration=integration, actor=bot_user, api_token=api_token, + metadata=metadata, + config=config, ) # Add bot user as a member of workspace @@ -139,6 +146,7 @@ def create(self, request, slug): status=status.HTTP_400_BAD_REQUEST, ) except (Workspace.DoesNotExist, Integration.DoesNotExist) as e: + capture_exception(e) return Response( {"error": "Workspace or Integration not found"}, status=status.HTTP_400_BAD_REQUEST, diff --git a/apiserver/plane/api/views/integration/github.py b/apiserver/plane/api/views/integration/github.py index f50275608ff..c37a4ca7897 100644 --- a/apiserver/plane/api/views/integration/github.py +++ b/apiserver/plane/api/views/integration/github.py @@ -143,51 +143,3 @@ def perform_create(self, serializer): project_id=self.kwargs.get("project_id"), issue_sync_id=self.kwargs.get("issue_sync_id"), ) - - -class GithubAppInstallationViewSet(APIView): - permission_classes = [AllowAny] - - def get_jwt_token(self): - app_id = os.environ.get("GITHUB_APP_ID", "") - secret = bytes(os.environ.get("GITHUB_APP_PRIVATE_KEY", ""), encoding="utf8") - current_timestamp = int(datetime.now().timestamp()) - due_date = datetime.now() + timedelta(minutes=10) - expiry = int(due_date.timestamp()) - payload = { - "iss": app_id, - "sub": app_id, - "exp": expiry, - "iat": current_timestamp, - "aud": "https://github.com/login/oauth/access_token", - } - - priv_rsakey = load_pem_private_key(secret, None, default_backend()) - token = jwt.encode(payload, priv_rsakey, algorithm="RS256") - return token - - def post(self, request, slug, installation_id): - token = self.get_jwt_token() - import requests - - url = f"https://api.github.com/app/installations/{installation_id}" - headers = { - "Authorization": "Bearer " + token, - "Accept": "application/vnd.github+json", - } - response = requests.get(url, headers=headers).json() - - # serializer = GithubRepositorySerializer( - # data={ - # "name": response.get("app_slug"), - # "url": response.get("html_url"), - # "repository_id": response.get("id"), - # "config": response, - # "owner": response.get("account").get("login"), - # } - # ) - - # if serializer.is_valid(raise_exception=True): - # serializer.save() - - return Response("Created") diff --git a/apiserver/plane/db/models/integration/base.py b/apiserver/plane/db/models/integration/base.py index 8eaa70809ec..47db0483c48 100644 --- a/apiserver/plane/db/models/integration/base.py +++ b/apiserver/plane/db/models/integration/base.py @@ -13,6 +13,7 @@ class Integration(AuditModel): id = models.UUIDField( default=uuid.uuid4, unique=True, editable=False, db_index=True, primary_key=True ) + title = models.CharField(max_length=400) provider = models.CharField(max_length=400, unique=True) network = models.PositiveIntegerField( default=1, choices=((1, "Private"), (2, "Public")) @@ -53,6 +54,8 @@ class WorkspaceIntegration(BaseModel): ) metadata = models.JSONField(default=dict) + config = models.JSONField(default=dict) + def __str__(self): """Return name of the integration and workspace""" return f"{self.workspace.name} <{self.integration.provider}>" diff --git a/apiserver/plane/utils/integrations/github.py b/apiserver/plane/utils/integrations/github.py new file mode 100644 index 00000000000..a3be4290e91 --- /dev/null +++ b/apiserver/plane/utils/integrations/github.py @@ -0,0 +1,37 @@ +import os +import jwt +import requests +from datetime import datetime, timedelta +from cryptography.hazmat.primitives.serialization import load_pem_private_key +from cryptography.hazmat.backends import default_backend + + +def get_jwt_token(): + app_id = os.environ.get("GITHUB_APP_ID", "") + secret = bytes(os.environ.get("GITHUB_APP_PRIVATE_KEY", ""), encoding="utf8") + current_timestamp = int(datetime.now().timestamp()) + due_date = datetime.now() + timedelta(minutes=10) + expiry = int(due_date.timestamp()) + payload = { + "iss": app_id, + "sub": app_id, + "exp": expiry, + "iat": current_timestamp, + "aud": "https://github.com/login/oauth/access_token", + } + + priv_rsakey = load_pem_private_key(secret, None, default_backend()) + token = jwt.encode(payload, priv_rsakey, algorithm="RS256") + return token + + +def get_github_metadata(installation_id): + token = get_jwt_token() + + url = f"https://api.github.com/app/installations/{installation_id}" + headers = { + "Authorization": "Bearer " + token, + "Accept": "application/vnd.github+json", + } + response = requests.get(url, headers=headers).json() + return response diff --git a/apps/app/components/popup/index.tsx b/apps/app/components/popup/index.tsx index 439aab62aaf..9a4b0075815 100644 --- a/apps/app/components/popup/index.tsx +++ b/apps/app/components/popup/index.tsx @@ -1,7 +1,7 @@ import { useRouter } from "next/router"; import React, { useRef } from "react"; -const OAuthPopUp = ({ workspaceSlug }: any) => { +const OAuthPopUp = ({ workspaceSlug, integration }: any) => { const popup = useRef(); const router = useRouter(); @@ -32,7 +32,7 @@ const OAuthPopUp = ({ workspaceSlug }: any) => { return ( <>
- +
); diff --git a/apps/app/constants/fetch-keys.ts b/apps/app/constants/fetch-keys.ts index 77df1bc97a0..8ce9fc72c97 100644 --- a/apps/app/constants/fetch-keys.ts +++ b/apps/app/constants/fetch-keys.ts @@ -3,6 +3,7 @@ export const USER_WORKSPACE_INVITATIONS = "USER_WORKSPACE_INVITATIONS"; export const USER_WORKSPACES = "USER_WORKSPACES"; export const WORKSPACE_DETAILS = (workspaceSlug: string) => `WORKSPACE_DETAILS_${workspaceSlug}`; +export const WORKSPACE_INTEGRATIONS = `WORKSPACE_INTEGRATIONS`; export const WORKSPACE_MEMBERS = (workspaceSlug: string) => `WORKSPACE_MEMBERS_${workspaceSlug}`; export const WORKSPACE_MEMBERS_ME = (workspaceSlug: string) => diff --git a/apps/app/pages/[workspaceSlug]/settings/integrations.tsx b/apps/app/pages/[workspaceSlug]/settings/integrations.tsx index d8cb88946d8..20982b94a8f 100644 --- a/apps/app/pages/[workspaceSlug]/settings/integrations.tsx +++ b/apps/app/pages/[workspaceSlug]/settings/integrations.tsx @@ -13,7 +13,7 @@ import workspaceService from "services/workspace.service"; import AppLayout from "layouts/app-layout"; // ui import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs"; -import { WORKSPACE_DETAILS } from "constants/fetch-keys"; +import { WORKSPACE_DETAILS, WORKSPACE_INTEGRATIONS } from "constants/fetch-keys"; import OAuthPopUp from "components/popup"; type TWorkspaceIntegrationsProps = { @@ -33,6 +33,10 @@ const WorkspaceIntegrations: NextPage = (props) => () => (workspaceSlug ? workspaceService.getWorkspace(workspaceSlug as string) : null) ); + const { data: integrations } = useSWR(workspaceSlug ? WORKSPACE_INTEGRATIONS : null, () => + workspaceSlug ? workspaceService.getIntegrations() : null + ); + return ( <> = (props) => } >
- {/* */} - + {integrations?.map((integration: any) => ( + + ))}
diff --git a/apps/app/pages/installations/github/index.tsx b/apps/app/pages/installations/[provider]/index.tsx similarity index 82% rename from apps/app/pages/installations/github/index.tsx rename to apps/app/pages/installations/[provider]/index.tsx index 325b424d6e2..85effe46b8b 100644 --- a/apps/app/pages/installations/github/index.tsx +++ b/apps/app/pages/installations/[provider]/index.tsx @@ -5,17 +5,19 @@ interface IGithuPostInstallationProps { installation_id: string; setup_action: string; state: string; + provider: string; } const AppPostInstallation = ({ installation_id, setup_action, state, + provider, }: IGithuPostInstallationProps) => { useEffect(() => { if (state && installation_id) { appinstallationsService - .addGithubApp(state, installation_id) + .addGithubApp(state, provider, { installation_id }) .then((res) => { window.opener = null; window.open("", "_self"); @@ -25,11 +27,12 @@ const AppPostInstallation = ({ console.log(err); }); } - }, [state, installation_id]); + }, [state, installation_id, provider]); return <>Loading...; }; export async function getServerSideProps(context: any) { + console.log(context.query); return { props: context.query, }; diff --git a/apps/app/services/appinstallations.service.ts b/apps/app/services/appinstallations.service.ts index 285818b5f76..3ceae3b1ad3 100644 --- a/apps/app/services/appinstallations.service.ts +++ b/apps/app/services/appinstallations.service.ts @@ -8,8 +8,8 @@ class AppInstallationsService extends APIService { super(NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000"); } - async addGithubApp(workspaceSlug: string, installation_id: string): Promise { - return this.post(`/api/workspaces/${workspaceSlug}/github-installations/${installation_id}/`) + async addGithubApp(workspaceSlug: string, provider: string, data: any): Promise { + return this.post(`/api/workspaces/${workspaceSlug}/workspace-integrations/${provider}/`, data) .then((response) => response?.data) .catch((error) => { throw error?.response; diff --git a/apps/app/services/workspace.service.ts b/apps/app/services/workspace.service.ts index 034104242ba..7f21b046415 100644 --- a/apps/app/services/workspace.service.ts +++ b/apps/app/services/workspace.service.ts @@ -169,6 +169,13 @@ class WorkspaceService extends APIService { throw error?.response?.data; }); } + async getIntegrations(): Promise { + return this.get(`/api/integrations/`) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } } export default new WorkspaceService(); From 14986755a83dd89a44d2aa09fd5c1a1a7c54f8e3 Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Wed, 15 Feb 2023 15:04:30 +0530 Subject: [PATCH 30/36] feat: github repositories endpoint --- apiserver/plane/api/urls.py | 5 +++ apiserver/plane/api/views/__init__.py | 1 + .../plane/api/views/integration/__init__.py | 1 + .../plane/api/views/integration/github.py | 43 +++++++++---------- .../plane/utils/integrations/__init__.py | 0 apiserver/plane/utils/integrations/github.py | 11 +++++ 6 files changed, 39 insertions(+), 22 deletions(-) create mode 100644 apiserver/plane/utils/integrations/__init__.py diff --git a/apiserver/plane/api/urls.py b/apiserver/plane/api/urls.py index 354740cd274..9260a9d9727 100644 --- a/apiserver/plane/api/urls.py +++ b/apiserver/plane/api/urls.py @@ -89,6 +89,7 @@ # Integrations IntegrationViewSet, WorkspaceIntegrationViewSet, + GithubRepositoriesEndpoint, GithubRepositorySyncViewSet, GithubIssueSyncViewSet, GithubCommentSyncViewSet, @@ -734,6 +735,10 @@ name="workspace-integrations", ), # Github Integrations + path( + "workspaces//workspace-integrations//github-repositories/", + GithubRepositoriesEndpoint.as_view(), + ), path( "workspaces//projects//workspace-integrations//github-repository-sync/", GithubRepositorySyncViewSet.as_view( diff --git a/apiserver/plane/api/views/__init__.py b/apiserver/plane/api/views/__init__.py index 62e050ec31e..275642c501c 100644 --- a/apiserver/plane/api/views/__init__.py +++ b/apiserver/plane/api/views/__init__.py @@ -80,4 +80,5 @@ GithubIssueSyncViewSet, GithubRepositorySyncViewSet, GithubCommentSyncViewSet, + GithubRepositoriesEndpoint, ) diff --git a/apiserver/plane/api/views/integration/__init__.py b/apiserver/plane/api/views/integration/__init__.py index 095e9d1f7f9..69320257368 100644 --- a/apiserver/plane/api/views/integration/__init__.py +++ b/apiserver/plane/api/views/integration/__init__.py @@ -3,4 +3,5 @@ GithubRepositorySyncViewSet, GithubIssueSyncViewSet, GithubCommentSyncViewSet, + GithubRepositoriesEndpoint, ) diff --git a/apiserver/plane/api/views/integration/github.py b/apiserver/plane/api/views/integration/github.py index c37a4ca7897..489668a3d3c 100644 --- a/apiserver/plane/api/views/integration/github.py +++ b/apiserver/plane/api/views/integration/github.py @@ -1,18 +1,10 @@ -import os -import jwt -from datetime import timedelta, datetime -from cryptography.hazmat.primitives.serialization import load_pem_private_key -from cryptography.hazmat.backends import default_backend - # Third party imports from rest_framework import status from rest_framework.response import Response -from rest_framework.views import APIView -from rest_framework.permissions import AllowAny from sentry_sdk import capture_exception # Module imports -from plane.api.views import BaseViewSet +from plane.api.views import BaseViewSet, BaseAPIView from plane.db.models import ( GithubIssueSync, GithubRepositorySync, @@ -27,6 +19,23 @@ GithubRepositorySyncSerializer, GithubCommentSyncSerializer, ) +from plane.utils.integrations.github import get_github_repos + + +class GithubRepositoriesEndpoint(BaseAPIView): + def get(self, request, slug, workspace_integration_id): + try: + workspace_integration = WorkspaceIntegration.objects.get( + workspace__slug=slug, pk=workspace_integration_id + ) + repository_url = workspace_integration.metadata["account"]["repos_url"] + repositories = get_github_repos(repository_url) + return Response(repositories, status=status.HTTP_200_OK) + except WorkspaceIntegration.DoesNotExist: + return Response( + {"error": "Workspace Integration Does not exists"}, + status=status.HTTP_400_BAD_REQUEST, + ) class GithubRepositorySyncViewSet(BaseViewSet): @@ -43,19 +52,10 @@ def create(self, request, slug, project_id, workspace_integration_id): config = request.data.get("config", {}) repository_id = request.data.get("repository_id", False) owner = request.data.get("owner", False) - installation_id = request.data.get("installation_id", False) - - if ( - not name - or not url - or not repository_id - or not owner - or not installation_id - ): + + if not name or not url or not repository_id or not owner: return Response( - { - "error": "Name, url, repository_id, owner and installation_id are required" - }, + {"error": "Name, url, repository_id and owner are required"}, status=status.HTTP_400_BAD_REQUEST, ) @@ -96,7 +96,6 @@ def create(self, request, slug, project_id, workspace_integration_id): credentials=request.data.get("credentials", {}), project_id=project_id, label=label, - installation_id=installation_id, ) # Add bot as a member in the project diff --git a/apiserver/plane/utils/integrations/__init__.py b/apiserver/plane/utils/integrations/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/apiserver/plane/utils/integrations/github.py b/apiserver/plane/utils/integrations/github.py index a3be4290e91..ae9e3ee9605 100644 --- a/apiserver/plane/utils/integrations/github.py +++ b/apiserver/plane/utils/integrations/github.py @@ -35,3 +35,14 @@ def get_github_metadata(installation_id): } response = requests.get(url, headers=headers).json() return response + + +def get_github_repos(repo_url): + token = get_jwt_token() + + headers = { + "Authorization": "Bearer " + token, + "Accept": "application/vnd.github+json", + } + response = requests.get(repo_url, headers=headers).json() + return response From 106df74654d56525ef34829b84dbeba08b42fb30 Mon Sep 17 00:00:00 2001 From: venplane Date: Thu, 16 Feb 2023 00:54:23 +0530 Subject: [PATCH 31/36] feat: github and project repos synchronisation --- .../plane/api/serializers/integration/base.py | 2 + apiserver/plane/api/urls.py | 10 +- apiserver/plane/api/views/integration/base.py | 2 +- .../plane/api/views/integration/github.py | 5 +- .../plane/db/models/integration/github.py | 1 - apiserver/plane/utils/integrations/github.py | 18 ++- apps/app/constants/fetch-keys.ts | 4 +- apps/app/layouts/app-layout/index.tsx | 4 + .../[projectId]/settings/integrations.tsx | 148 ++++++++++++++++++ .../[workspaceSlug]/settings/integrations.tsx | 4 +- apps/app/services/project.service.ts | 31 ++++ apps/app/services/workspace.service.ts | 7 + 12 files changed, 226 insertions(+), 10 deletions(-) create mode 100644 apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/integrations.tsx diff --git a/apiserver/plane/api/serializers/integration/base.py b/apiserver/plane/api/serializers/integration/base.py index f2ff83482d4..10ebd462012 100644 --- a/apiserver/plane/api/serializers/integration/base.py +++ b/apiserver/plane/api/serializers/integration/base.py @@ -13,6 +13,8 @@ class Meta: class WorkspaceIntegrationSerializer(BaseSerializer): + integration_detail = IntegrationSerializer(read_only=True, source="integration") + class Meta: model = WorkspaceIntegration fields = "__all__" diff --git a/apiserver/plane/api/urls.py b/apiserver/plane/api/urls.py index 9260a9d9727..e44579cb759 100644 --- a/apiserver/plane/api/urls.py +++ b/apiserver/plane/api/urls.py @@ -715,10 +715,18 @@ name="integrations", ), path( - "workspaces//workspace-integrations//", + "workspaces//workspace-integrations/", WorkspaceIntegrationViewSet.as_view( { "get": "list", + } + ), + name="workspace-integrations", + ), + path( + "workspaces//workspace-integrations//", + WorkspaceIntegrationViewSet.as_view( + { "post": "create", } ), diff --git a/apiserver/plane/api/views/integration/base.py b/apiserver/plane/api/views/integration/base.py index 79a6d089d17..a7a266430af 100644 --- a/apiserver/plane/api/views/integration/base.py +++ b/apiserver/plane/api/views/integration/base.py @@ -96,7 +96,7 @@ def create(self, request, slug, provider): # Create a bot user bot_user = User.objects.create( - email=f"{str(integration.id)}-{str(workspace.id)}@plane.so", + email=f"{uuid.uuid4().hex}@plane.so", username=uuid.uuid4().hex, password=make_password(uuid.uuid4().hex), is_password_autoset=True, diff --git a/apiserver/plane/api/views/integration/github.py b/apiserver/plane/api/views/integration/github.py index 489668a3d3c..7486ce7b929 100644 --- a/apiserver/plane/api/views/integration/github.py +++ b/apiserver/plane/api/views/integration/github.py @@ -28,8 +28,9 @@ def get(self, request, slug, workspace_integration_id): workspace_integration = WorkspaceIntegration.objects.get( workspace__slug=slug, pk=workspace_integration_id ) - repository_url = workspace_integration.metadata["account"]["repos_url"] - repositories = get_github_repos(repository_url) + access_tokens_url = workspace_integration.metadata["access_tokens_url"] + repositories_url = workspace_integration.metadata["repositories_url"] + repositories = get_github_repos(access_tokens_url, repositories_url) return Response(repositories, status=status.HTTP_200_OK) except WorkspaceIntegration.DoesNotExist: return Response( diff --git a/apiserver/plane/db/models/integration/github.py b/apiserver/plane/db/models/integration/github.py index 07935056d1f..130925c21d6 100644 --- a/apiserver/plane/db/models/integration/github.py +++ b/apiserver/plane/db/models/integration/github.py @@ -28,7 +28,6 @@ class Meta: class GithubRepositorySync(ProjectBaseModel): - installation_id = models.BigIntegerField() repository = models.OneToOneField( "db.GithubRepository", on_delete=models.CASCADE, related_name="syncs" ) diff --git a/apiserver/plane/utils/integrations/github.py b/apiserver/plane/utils/integrations/github.py index ae9e3ee9605..ba9cb0ae01a 100644 --- a/apiserver/plane/utils/integrations/github.py +++ b/apiserver/plane/utils/integrations/github.py @@ -37,12 +37,26 @@ def get_github_metadata(installation_id): return response -def get_github_repos(repo_url): +def get_github_repos(access_tokens_url, repositories_url): token = get_jwt_token() headers = { "Authorization": "Bearer " + token, "Accept": "application/vnd.github+json", } - response = requests.get(repo_url, headers=headers).json() + + oauth_response = requests.post( + access_tokens_url, + headers=headers, + ).json() + + oauth_token = oauth_response.get("token") + headers = { + "Authorization": "Bearer " + oauth_token, + "Accept": "application/vnd.github+json", + } + response = requests.get( + repositories_url, + headers=headers, + ).json() return response diff --git a/apps/app/constants/fetch-keys.ts b/apps/app/constants/fetch-keys.ts index 8ce9fc72c97..e7360461daf 100644 --- a/apps/app/constants/fetch-keys.ts +++ b/apps/app/constants/fetch-keys.ts @@ -1,9 +1,11 @@ export const CURRENT_USER = "CURRENT_USER"; export const USER_WORKSPACE_INVITATIONS = "USER_WORKSPACE_INVITATIONS"; export const USER_WORKSPACES = "USER_WORKSPACES"; +export const APP_INTEGRATIONS = "APP_INTEGRATIONS"; export const WORKSPACE_DETAILS = (workspaceSlug: string) => `WORKSPACE_DETAILS_${workspaceSlug}`; -export const WORKSPACE_INTEGRATIONS = `WORKSPACE_INTEGRATIONS`; +export const WORKSPACE_INTEGRATIONS = (workspaceSlug: string) => + `WORKSPACE_INTEGRATIONS_${workspaceSlug}`; export const WORKSPACE_MEMBERS = (workspaceSlug: string) => `WORKSPACE_MEMBERS_${workspaceSlug}`; export const WORKSPACE_MEMBERS_ME = (workspaceSlug: string) => diff --git a/apps/app/layouts/app-layout/index.tsx b/apps/app/layouts/app-layout/index.tsx index d6df89a2f06..db76abf072f 100644 --- a/apps/app/layouts/app-layout/index.tsx +++ b/apps/app/layouts/app-layout/index.tsx @@ -98,6 +98,10 @@ const sidebarLinks: ( label: "Labels", href: `/${workspaceSlug}/projects/${projectId}/settings/labels`, }, + { + label: "Integrations", + href: `/${workspaceSlug}/projects/${projectId}/settings/integrations`, + }, ]; const AppLayout: FC = ({ diff --git a/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/integrations.tsx b/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/integrations.tsx new file mode 100644 index 00000000000..4e472d7e560 --- /dev/null +++ b/apps/app/pages/[workspaceSlug]/projects/[projectId]/settings/integrations.tsx @@ -0,0 +1,148 @@ +import React, { useEffect, useState } from "react"; + +import { useRouter } from "next/router"; +import Image from "next/image"; + +import useSWR, { mutate } from "swr"; + +// lib +import { requiredAdmin } from "lib/auth"; +// layouts +import AppLayout from "layouts/app-layout"; +// services +import workspaceService from "services/workspace.service"; +import projectService from "services/project.service"; + +import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs"; +// types +import { IProject, IWorkspace } from "types"; +import type { NextPageContext, NextPage } from "next"; +// fetch-keys +import { PROJECT_DETAILS, WORKSPACE_INTEGRATIONS } from "constants/fetch-keys"; + +type TProjectIntegrationsProps = { + isMember: boolean; + isOwner: boolean; + isViewer: boolean; + isGuest: boolean; +}; + +const defaultValues: Partial = { + project_lead: null, + default_assignee: null, +}; + +const ProjectIntegrations: NextPage = (props) => { + const { isMember, isOwner, isViewer, isGuest } = props; + const [userRepos, setUserRepos] = useState([]); + const [activeIntegrationId, setActiveIntegrationId] = useState(); + + const { + query: { workspaceSlug, projectId }, + } = useRouter(); + + const { data: projectDetails } = useSWR( + workspaceSlug && projectId ? PROJECT_DETAILS(projectId as string) : null, + workspaceSlug && projectId + ? () => projectService.getProject(workspaceSlug as string, projectId as string) + : null + ); + + const { data: integrations } = useSWR( + workspaceSlug ? WORKSPACE_INTEGRATIONS(workspaceSlug as string) : null, + () => + workspaceSlug ? workspaceService.getWorkspaceIntegrations(workspaceSlug as string) : null + ); + const handleChange = (repo: any) => { + const { + html_url, + owner: { login }, + id, + name, + } = repo; + + projectService + .syncGiuthubRepository( + workspaceSlug as string, + projectId as string, + activeIntegrationId as any, + { name, owner: login, repository_id: id, url: html_url } + ) + .then((res) => { + console.log(res); + }) + .catch((err) => { + console.log(err); + }); + }; + console.log(userRepos); + return ( + + + + + } + > +
+ {integrations?.map((integration: any) => ( +
{ + setActiveIntegrationId(integration.id); + projectService + .getGithubRepositories(workspaceSlug as any, integration.id) + .then((response) => { + setUserRepos(response.repositories); + }) + .catch((err) => { + console.log(err); + }); + }} + > + {integration.integration_detail.provider} +
+ ))} + {userRepos.length > 0 && ( + + )} +
+
+ ); +}; + +export const getServerSideProps = async (ctx: NextPageContext) => { + const projectId = ctx.query.projectId as string; + const workspaceSlug = ctx.query.workspaceSlug as string; + + const memberDetail = await requiredAdmin(workspaceSlug, projectId, ctx.req?.headers.cookie); + + return { + props: { + isOwner: memberDetail?.role === 20, + isMember: memberDetail?.role === 15, + isViewer: memberDetail?.role === 10, + isGuest: memberDetail?.role === 5, + }, + }; +}; + +export default ProjectIntegrations; diff --git a/apps/app/pages/[workspaceSlug]/settings/integrations.tsx b/apps/app/pages/[workspaceSlug]/settings/integrations.tsx index 20982b94a8f..0757dfd5273 100644 --- a/apps/app/pages/[workspaceSlug]/settings/integrations.tsx +++ b/apps/app/pages/[workspaceSlug]/settings/integrations.tsx @@ -13,7 +13,7 @@ import workspaceService from "services/workspace.service"; import AppLayout from "layouts/app-layout"; // ui import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs"; -import { WORKSPACE_DETAILS, WORKSPACE_INTEGRATIONS } from "constants/fetch-keys"; +import { WORKSPACE_DETAILS, APP_INTEGRATIONS } from "constants/fetch-keys"; import OAuthPopUp from "components/popup"; type TWorkspaceIntegrationsProps = { @@ -33,7 +33,7 @@ const WorkspaceIntegrations: NextPage = (props) => () => (workspaceSlug ? workspaceService.getWorkspace(workspaceSlug as string) : null) ); - const { data: integrations } = useSWR(workspaceSlug ? WORKSPACE_INTEGRATIONS : null, () => + const { data: integrations } = useSWR(workspaceSlug ? APP_INTEGRATIONS : null, () => workspaceSlug ? workspaceService.getIntegrations() : null ); diff --git a/apps/app/services/project.service.ts b/apps/app/services/project.service.ts index c67f8144a57..d2f3aa19333 100644 --- a/apps/app/services/project.service.ts +++ b/apps/app/services/project.service.ts @@ -201,6 +201,37 @@ class ProjectServices extends APIService { throw error?.response?.data; }); } + + async getGithubRepositories(slug: string, workspaceIntegrationId: string): Promise { + return this.get( + `/api/workspaces/${slug}/workspace-integrations/${workspaceIntegrationId}/github-repositories/` + ) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async syncGiuthubRepository( + slug: string, + projectId: string, + workspaceIntegrationId: string, + data: { + name: string; + owner: string; + repository_id: string; + url: string; + } + ): Promise { + return this.post( + `/api/workspaces/${slug}/projects/${projectId}/workspace-integrations/${workspaceIntegrationId}/github-repository-sync/`, + data + ) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } } export default new ProjectServices(); diff --git a/apps/app/services/workspace.service.ts b/apps/app/services/workspace.service.ts index 7f21b046415..cf3f6d3e987 100644 --- a/apps/app/services/workspace.service.ts +++ b/apps/app/services/workspace.service.ts @@ -176,6 +176,13 @@ class WorkspaceService extends APIService { throw error?.response?.data; }); } + async getWorkspaceIntegrations(slug: string): Promise { + return this.get(`/api/workspaces/${slug}/workspace-integrations/`) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } } export default new WorkspaceService(); From fc0b6940b2b8381034737b0426b5587a0d1990fd Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Thu, 16 Feb 2023 13:34:07 +0530 Subject: [PATCH 32/36] feat: delete issue and delete comment activity --- apiserver/plane/api/views/issue.py | 43 +++++++++++++++++++ .../plane/bgtasks/issue_activites_task.py | 34 +++++++++++++++ apiserver/plane/db/models/issue.py | 2 +- 3 files changed, 78 insertions(+), 1 deletion(-) diff --git a/apiserver/plane/api/views/issue.py b/apiserver/plane/api/views/issue.py index e45b6c87fb4..a6f46a37148 100644 --- a/apiserver/plane/api/views/issue.py +++ b/apiserver/plane/api/views/issue.py @@ -90,6 +90,27 @@ def perform_update(self, serializer): return super().perform_update(serializer) + def perform_destroy(self, instance): + current_instance = ( + self.get_queryset().filter(pk=self.kwargs.get("pk", None)).first() + ) + if current_instance is not None: + issue_activity.delay( + { + "type": "issue.activity.deleted", + "requested_data": json.dumps( + {"issue_id": str(self.kwargs.get("pk", None))} + ), + "actor_id": str(self.request.user.id), + "issue_id": str(self.kwargs.get("pk", None)), + "project_id": str(self.kwargs.get("project_id", None)), + "current_instance": json.dumps( + IssueSerializer(current_instance).data, cls=DjangoJSONEncoder + ), + }, + ) + return super().perform_destroy(instance) + def get_queryset(self): return ( super() @@ -385,6 +406,28 @@ def perform_update(self, serializer): return super().perform_update(serializer) + def perform_destroy(self, instance): + current_instance = ( + self.get_queryset().filter(pk=self.kwargs.get("pk", None)).first() + ) + if current_instance is not None: + issue_activity.delay( + { + "type": "comment.activity.deleted", + "requested_data": json.dumps( + {"comment_id": str(self.kwargs.get("pk", None))} + ), + "actor_id": str(self.request.user.id), + "issue_id": str(self.kwargs.get("issue_id", None)), + "project_id": str(self.kwargs.get("project_id", None)), + "current_instance": json.dumps( + IssueCommentSerializer(current_instance).data, + cls=DjangoJSONEncoder, + ), + }, + ) + return super().perform_destroy(instance) + def get_queryset(self): return self.filter_queryset( super() diff --git a/apiserver/plane/bgtasks/issue_activites_task.py b/apiserver/plane/bgtasks/issue_activites_task.py index 9cdc4254727..a7ea205d77b 100644 --- a/apiserver/plane/bgtasks/issue_activites_task.py +++ b/apiserver/plane/bgtasks/issue_activites_task.py @@ -705,6 +705,38 @@ def update_comment_activity( ) +def delete_issue_activity( + requested_data, current_instance, issue_id, project, actor, issue_activities +): + print("Delete ISsue") + issue_activities.append( + IssueActivity( + project=project, + workspace=project.workspace, + comment=f"{actor.email} deleted the issue", + verb="deleted", + actor=actor, + field="issue", + ) + ) + + +def delete_comment_activity( + requested_data, current_instance, issue_id, project, actor, issue_activities +): + issue_activities.append( + IssueActivity( + issue_id=issue_id, + project=project, + workspace=project.workspace, + comment=f"{actor.email} deleted the comment", + verb="deleted", + actor=actor, + field="comment", + ) + ) + + # Receive message from room group @job("default") def issue_activity(event): @@ -728,8 +760,10 @@ def issue_activity(event): ACTIVITY_MAPPER = { "issue.activity.created": create_issue_activity, "issue.activity.updated": update_issue_activity, + "issue.activity.deleted": delete_issue_activity, "comment.activity.created": create_comment_activity, "comment.activity.updated": update_comment_activity, + "comment.activity.deleted": delete_comment_activity, } func = ACTIVITY_MAPPER.get(type) diff --git a/apiserver/plane/db/models/issue.py b/apiserver/plane/db/models/issue.py index 82e8343bb36..1b363299f9d 100644 --- a/apiserver/plane/db/models/issue.py +++ b/apiserver/plane/db/models/issue.py @@ -163,7 +163,7 @@ def __str__(self): class IssueActivity(ProjectBaseModel): issue = models.ForeignKey( - Issue, on_delete=models.CASCADE, related_name="issue_activity" + Issue, on_delete=models.SET_NULL, null=True, related_name="issue_activity" ) verb = models.CharField(max_length=255, verbose_name="Action", default="created") field = models.CharField( From 24cc52dee56c6cafee95130783bcb990bb5a7fac Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Thu, 16 Feb 2023 13:34:52 +0530 Subject: [PATCH 33/36] refactor: remove print logs --- apiserver/plane/bgtasks/issue_activites_task.py | 1 - 1 file changed, 1 deletion(-) diff --git a/apiserver/plane/bgtasks/issue_activites_task.py b/apiserver/plane/bgtasks/issue_activites_task.py index a7ea205d77b..a9bf30712d9 100644 --- a/apiserver/plane/bgtasks/issue_activites_task.py +++ b/apiserver/plane/bgtasks/issue_activites_task.py @@ -708,7 +708,6 @@ def update_comment_activity( def delete_issue_activity( requested_data, current_instance, issue_id, project, actor, issue_activities ): - print("Delete ISsue") issue_activities.append( IssueActivity( project=project, From d848d5f4727e152a8d1c6fa25e10a94fce4ebc74 Mon Sep 17 00:00:00 2001 From: venplane Date: Thu, 16 Feb 2023 23:56:46 +0530 Subject: [PATCH 34/36] FIX: reading env names for github app while installation --- app.json | 14 +++++++++++--- apps/app/components/popup/index.tsx | 2 +- turbo.json | 18 +++++++++++++----- 3 files changed, 25 insertions(+), 9 deletions(-) diff --git a/app.json b/app.json index 01791192018..7f6b274273c 100644 --- a/app.json +++ b/app.json @@ -6,8 +6,16 @@ "website": "https://plane.so/", "success_url": "/", "stack": "heroku-22", - "keywords": ["plane", "project management", "django", "next"], - "addons": ["heroku-postgresql:mini", "heroku-redis:mini"], + "keywords": [ + "plane", + "project management", + "django", + "next" + ], + "addons": [ + "heroku-postgresql:mini", + "heroku-redis:mini" + ], "buildpacks": [ { "url": "https://github.com/heroku/heroku-buildpack-python.git" @@ -74,4 +82,4 @@ "value": "" } } -} +} \ No newline at end of file diff --git a/apps/app/components/popup/index.tsx b/apps/app/components/popup/index.tsx index 9a4b0075815..e97d39493d7 100644 --- a/apps/app/components/popup/index.tsx +++ b/apps/app/components/popup/index.tsx @@ -19,7 +19,7 @@ const OAuthPopUp = ({ workspaceSlug, integration }: any) => { height = 600; const left = window.innerWidth / 2 - width / 2; const top = window.innerHeight / 2 - height / 2; - const url = `https://github.com/apps/plane-web-local/installations/new?state=${workspaceSlug}`; + const url = `https://github.com/apps/${process.env.NEXT_PUBLIC_GITHUB_APP_NAME}/installations/new?state=${workspaceSlug}`; return window.open(url, "", `width=${width}, height=${height}, top=${top}, left=${left}`); }; diff --git a/turbo.json b/turbo.json index f20ceecdf84..d435df2d515 100644 --- a/turbo.json +++ b/turbo.json @@ -9,15 +9,23 @@ "NEXT_PUBLIC_DOCSEARCH_INDEX_NAME", "NEXT_PUBLIC_SENTRY_DSN", "SENTRY_AUTH_TOKEN", - "NEXT_PUBLIC_SENTRY_ENVIRONMENT" + "NEXT_PUBLIC_SENTRY_ENVIRONMENT", + "NEXT_PUBLIC_GITHUB_APP_NAME" ], "pipeline": { "build": { - "dependsOn": ["^build"], - "outputs": [".next/**", "dist/**"] + "dependsOn": [ + "^build" + ], + "outputs": [ + ".next/**", + "dist/**" + ] }, "test": { - "dependsOn": ["^build"], + "dependsOn": [ + "^build" + ], "outputs": [] }, "lint": { @@ -33,4 +41,4 @@ "cache": false } } -} +} \ No newline at end of file From b8de8383b1eb402b06ab43e764772c952f6f54bd Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Fri, 17 Feb 2023 11:16:11 +0530 Subject: [PATCH 35/36] refactor: update bot user firstname with title --- apiserver/plane/api/views/integration/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apiserver/plane/api/views/integration/base.py b/apiserver/plane/api/views/integration/base.py index a7a266430af..bded732ec6a 100644 --- a/apiserver/plane/api/views/integration/base.py +++ b/apiserver/plane/api/views/integration/base.py @@ -101,7 +101,7 @@ def create(self, request, slug, provider): password=make_password(uuid.uuid4().hex), is_password_autoset=True, is_bot=True, - first_name=integration.provider, + first_name=integration.title, avatar=integration.avatar_url if integration.avatar_url is not None else "", From 32aef5daebafcadfb1fea4e7b2248e4947a4965d Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Wed, 22 Feb 2023 12:51:02 +0530 Subject: [PATCH 36/36] fix: add is_bot value in field --- apiserver/plane/api/serializers/user.py | 1 + 1 file changed, 1 insertion(+) diff --git a/apiserver/plane/api/serializers/user.py b/apiserver/plane/api/serializers/user.py index dea0738cced..14a33d9c313 100644 --- a/apiserver/plane/api/serializers/user.py +++ b/apiserver/plane/api/serializers/user.py @@ -35,6 +35,7 @@ class Meta: "last_name", "email", "avatar", + "is_bot", ] read_only_fields = [ "id",