refactor: performance booster optimization (#176)

* refactor: setup multiple select related

* chore: upgrade sentry sdk to latest version

* refactor: update module and cycle views to increase performance

* refactor: remove pagination and make the response simillar to paginated API

* fix: update staging to DEBUG True for all logging

* refactor: update the query count print statement

* refactor: my issues endpoint to remove n+1

* refactor: optimize queries for workspace and project

* fix: project member endpoint

* fix: revert back workspace members

* refactor: update base file to remove workspace and project query and update permission layer accordingly

* refactor: update read_only fields in read serializers

* fix: read only serializers

* chore: update drf package

* revert: drf version upgrade

* revert: read only fields update

* revert: update serializer to old state

* chore: update drf to latest version

* refactor: update dispatch to display method as well

* refactor: optimize  cycle and module issue queries

* refactor: optimize module endpoint and issue list endpoint

* refactor: update prefetch related in modules and cycles

* refactor: create permission mapping in permission file
This commit is contained in:
pablohashescobar 2023-01-17 01:50:27 +05:30 committed by GitHub
parent f12b7ef923
commit 894e26116b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 126 additions and 80 deletions

View File

@ -4,6 +4,12 @@ from rest_framework.permissions import BasePermission, SAFE_METHODS
# Module import # Module import
from plane.db.models import WorkspaceMember, ProjectMember from plane.db.models import WorkspaceMember, ProjectMember
# Permission Mappings
Admin = 20
Member = 15
Viewer = 10
Guest = 5
class ProjectBasePermission(BasePermission): class ProjectBasePermission(BasePermission):
def has_permission(self, request, view): def has_permission(self, request, view):
@ -22,14 +28,14 @@ class ProjectBasePermission(BasePermission):
return WorkspaceMember.objects.filter( return WorkspaceMember.objects.filter(
workspace__slug=view.workspace_slug, workspace__slug=view.workspace_slug,
member=request.user, member=request.user,
role__in=[15, 20], role__in=[Admin, Member],
).exists() ).exists()
## Only Project Admins can update project attributes ## Only Project Admins can update project attributes
return ProjectMember.objects.filter( return ProjectMember.objects.filter(
workspace__slug=view.workspace_slug, workspace__slug=view.workspace_slug,
member=request.user, member=request.user,
role=20, role=Admin,
project_id=view.project_id, project_id=view.project_id,
).exists() ).exists()
@ -50,14 +56,14 @@ class ProjectMemberPermission(BasePermission):
return WorkspaceMember.objects.filter( return WorkspaceMember.objects.filter(
workspace__slug=view.workspace_slug, workspace__slug=view.workspace_slug,
member=request.user, member=request.user,
role__in=[15, 20], role__in=[Admin, Member],
).exists() ).exists()
## Only Project Admins can update project attributes ## Only Project Admins can update project attributes
return ProjectMember.objects.filter( return ProjectMember.objects.filter(
workspace__slug=view.workspace_slug, workspace__slug=view.workspace_slug,
member=request.user, member=request.user,
role__in=[15, 20], role__in=[Admin, Member],
project_id=view.project_id, project_id=view.project_id,
).exists() ).exists()
@ -80,6 +86,6 @@ class ProjectEntityPermission(BasePermission):
return ProjectMember.objects.filter( return ProjectMember.objects.filter(
workspace__slug=view.workspace_slug, workspace__slug=view.workspace_slug,
member=request.user, member=request.user,
role__in=[15, 20], role__in=[Admin, Member],
project_id=view.project_id, project_id=view.project_id,
).exists() ).exists()

View File

@ -2,7 +2,15 @@
from rest_framework.permissions import BasePermission, SAFE_METHODS from rest_framework.permissions import BasePermission, SAFE_METHODS
# Module imports # Module imports
from plane.db.models import WorkspaceMember, ProjectMember from plane.db.models import WorkspaceMember
# Permission Mappings
Owner = 20
Admin = 15
Member = 10
Guest = 5
# TODO: Move the below logic to python match - python v3.10 # TODO: Move the below logic to python match - python v3.10
@ -22,13 +30,15 @@ class WorkSpaceBasePermission(BasePermission):
# allow only admins and owners to update the workspace settings # allow only admins and owners to update the workspace settings
if request.method in ["PUT", "PATCH"]: if request.method in ["PUT", "PATCH"]:
return WorkspaceMember.objects.filter( return WorkspaceMember.objects.filter(
member=request.user, workspace=view.workspace, role__in=[15, 20] member=request.user,
workspace__slug=view.workspace_slug,
role__in=[Owner, Admin],
).exists() ).exists()
# allow only owner to delete the workspace # allow only owner to delete the workspace
if request.method == "DELETE": if request.method == "DELETE":
return WorkspaceMember.objects.filter( return WorkspaceMember.objects.filter(
member=request.user, workspace=view.workspace, role=20 member=request.user, workspace__slug=view.workspace_slug, role=Owner
).exists() ).exists()
@ -39,5 +49,7 @@ class WorkSpaceAdminPermission(BasePermission):
return False return False
return WorkspaceMember.objects.filter( return WorkspaceMember.objects.filter(
member=request.user, workspace=view.workspace, role__in=[15, 20] member=request.user,
workspace__slug=view.workspace_slug,
role__in=[Owner, Admin],
).exists() ).exists()

View File

@ -29,7 +29,6 @@ from .issue import (
IssueCommentSerializer, IssueCommentSerializer,
TimeLineIssueSerializer, TimeLineIssueSerializer,
IssuePropertySerializer, IssuePropertySerializer,
IssueLabelSerializer,
BlockerIssueSerializer, BlockerIssueSerializer,
BlockedIssueSerializer, BlockedIssueSerializer,
IssueAssigneeSerializer, IssueAssigneeSerializer,

View File

@ -443,4 +443,4 @@ class IssueSerializer(BaseSerializer):
"updated_by", "updated_by",
"created_at", "created_at",
"updated_at", "updated_at",
] ]

View File

@ -1,6 +1,7 @@
# Django imports # Django imports
from django.urls import resolve from django.urls import resolve
from django.conf import settings from django.conf import settings
# Third part imports # Third part imports
from rest_framework import status from rest_framework import status
from rest_framework.viewsets import ModelViewSet from rest_framework.viewsets import ModelViewSet
@ -39,32 +40,23 @@ class BaseViewSet(ModelViewSet, BasePaginator):
return self.model.objects.all() return self.model.objects.all()
except Exception as e: except Exception as e:
print(e) print(e)
raise APIException( raise APIException("Please check the view", status.HTTP_400_BAD_REQUEST)
"Please check the view", status.HTTP_400_BAD_REQUEST
)
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
response = super().dispatch(request, *args, **kwargs) response = super().dispatch(request, *args, **kwargs)
if settings.DEBUG: if settings.DEBUG:
from django.db import connection from django.db import connection
print(f'# of Queries: {len(connection.queries)}')
print(
f"{request.method} - {request.get_full_path()} of Queries: {len(connection.queries)}"
)
return response return response
@property @property
def workspace_slug(self): def workspace_slug(self):
return self.kwargs.get("slug", None) return self.kwargs.get("slug", None)
@property
def workspace(self):
if self.workspace_slug:
try:
return Workspace.objects.get(slug=self.workspace_slug)
except Workspace.DoesNotExist:
raise NotFound(detail="Workspace does not exist")
else:
return None
@property @property
def project_id(self): def project_id(self):
project_id = self.kwargs.get("project_id", None) project_id = self.kwargs.get("project_id", None)
@ -74,16 +66,6 @@ class BaseViewSet(ModelViewSet, BasePaginator):
if resolve(self.request.path_info).url_name == "project": if resolve(self.request.path_info).url_name == "project":
return self.kwargs.get("pk", None) return self.kwargs.get("pk", None)
@property
def project(self):
if self.project_id:
try:
return Project.objects.get(pk=self.project_id)
except Project.DoesNotExist:
raise NotFound(detail="Project does not exist")
else:
return None
class BaseAPIView(APIView, BasePaginator): class BaseAPIView(APIView, BasePaginator):
@ -110,33 +92,16 @@ class BaseAPIView(APIView, BasePaginator):
if settings.DEBUG: if settings.DEBUG:
from django.db import connection from django.db import connection
print(f'# of Queries: {len(connection.queries)}')
print(
f"{request.method} - {request.get_full_path()} of Queries: {len(connection.queries)}"
)
return response return response
@property @property
def workspace_slug(self): def workspace_slug(self):
return self.kwargs.get("slug", None) return self.kwargs.get("slug", None)
@property
def workspace(self):
if self.workspace_slug:
try:
return Workspace.objects.get(slug=self.workspace_slug)
except Workspace.DoesNotExist:
raise NotFound(detail="Workspace does not exist")
else:
return None
@property @property
def project_id(self): def project_id(self):
return self.kwargs.get("project_id", None) return self.kwargs.get("project_id", None)
@property
def project(self):
if self.project_id:
try:
return Project.objects.get(pk=self.project_id)
except Project.DoesNotExist:
raise NotFound(detail="Project does not exist")
else:
return None

View File

@ -32,6 +32,7 @@ class CycleViewSet(BaseViewSet):
.filter(project__project_projectmember__member=self.request.user) .filter(project__project_projectmember__member=self.request.user)
.select_related("project") .select_related("project")
.select_related("workspace") .select_related("workspace")
.select_related("owned_by")
.distinct() .distinct()
) )
@ -62,8 +63,8 @@ class CycleIssueViewSet(BaseViewSet):
.select_related("project") .select_related("project")
.select_related("workspace") .select_related("workspace")
.select_related("cycle") .select_related("cycle")
.select_related("issue") .select_related("issue", "issue__state", "issue__project")
.select_related("issue__state") .prefetch_related("issue__assignees", "issue__labels")
.distinct() .distinct()
) )

View File

@ -125,7 +125,9 @@ class IssueViewSet(BaseViewSet):
.prefetch_related( .prefetch_related(
Prefetch( Prefetch(
"issue_module", "issue_module",
queryset=ModuleIssue.objects.select_related("module", "issue"), queryset=ModuleIssue.objects.select_related(
"module", "issue"
).prefetch_related("module__members"),
), ),
) )
) )
@ -161,10 +163,18 @@ class IssueViewSet(BaseViewSet):
return Response(issue_dict, status=status.HTTP_200_OK) return Response(issue_dict, status=status.HTTP_200_OK)
return self.paginate( return Response(
request=request, {
queryset=issue_queryset, "next_cursor": str(0),
on_results=lambda issues: IssueSerializer(issues, many=True).data, "prev_cursor": str(0),
"next_page_results": False,
"prev_page_results": False,
"count": issue_queryset.count(),
"total_pages": 1,
"extra_stats": {},
"results": IssueSerializer(issue_queryset, many=True).data,
},
status=status.HTTP_200_OK,
) )
except Exception as e: except Exception as e:
@ -206,8 +216,42 @@ class IssueViewSet(BaseViewSet):
class UserWorkSpaceIssues(BaseAPIView): class UserWorkSpaceIssues(BaseAPIView):
def get(self, request, slug): def get(self, request, slug):
try: try:
issues = Issue.objects.filter( issues = (
assignees__in=[request.user], workspace__slug=slug Issue.objects.filter(assignees__in=[request.user], workspace__slug=slug)
.select_related("project")
.select_related("workspace")
.select_related("state")
.select_related("parent")
.prefetch_related("assignees")
.prefetch_related("labels")
.prefetch_related(
Prefetch(
"blocked_issues",
queryset=IssueBlocker.objects.select_related(
"blocked_by", "block"
),
)
)
.prefetch_related(
Prefetch(
"blocker_issues",
queryset=IssueBlocker.objects.select_related(
"block", "blocked_by"
),
)
)
.prefetch_related(
Prefetch(
"issue_cycle",
queryset=CycleIssue.objects.select_related("cycle", "issue"),
),
)
.prefetch_related(
Prefetch(
"issue_module",
queryset=ModuleIssue.objects.select_related("module", "issue"),
),
)
) )
serializer = IssueSerializer(issues, many=True) serializer = IssueSerializer(issues, many=True)
return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.data, status=status.HTTP_200_OK)

View File

@ -15,7 +15,13 @@ from plane.api.serializers import (
ModuleIssueSerializer, ModuleIssueSerializer,
) )
from plane.api.permissions import ProjectEntityPermission from plane.api.permissions import ProjectEntityPermission
from plane.db.models import Module, ModuleIssue, Project, Issue, ModuleLink from plane.db.models import (
Module,
ModuleIssue,
Project,
Issue,
ModuleLink,
)
class ModuleViewSet(BaseViewSet): class ModuleViewSet(BaseViewSet):
@ -45,13 +51,15 @@ class ModuleViewSet(BaseViewSet):
.prefetch_related( .prefetch_related(
Prefetch( Prefetch(
"issue_module", "issue_module",
queryset=ModuleIssue.objects.select_related("module", "issue"), queryset=ModuleIssue.objects.select_related(
"module", "issue", "issue__state", "issue__project"
).prefetch_related("issue__assignees", "issue__labels"),
) )
) )
.prefetch_related( .prefetch_related(
Prefetch( Prefetch(
"link_module", "link_module",
queryset=ModuleLink.objects.select_related("module"), queryset=ModuleLink.objects.select_related("module", "created_by"),
) )
) )
) )
@ -117,7 +125,9 @@ class ModuleIssueViewSet(BaseViewSet):
.select_related("project") .select_related("project")
.select_related("workspace") .select_related("workspace")
.select_related("module") .select_related("module")
.select_related("issue") .select_related("issue", "issue__state", "issue__project")
.prefetch_related("issue__assignees", "issue__labels")
.prefetch_related("module__members")
.distinct() .distinct()
) )
@ -164,4 +174,4 @@ class ModuleIssueViewSet(BaseViewSet):
return Response( return Response(
{"error": "Something went wrong please try again later"}, {"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
) )

View File

@ -67,7 +67,9 @@ class ProjectViewSet(BaseViewSet):
.get_queryset() .get_queryset()
.filter(workspace__slug=self.kwargs.get("slug")) .filter(workspace__slug=self.kwargs.get("slug"))
.filter(Q(project_projectmember__member=self.request.user) | Q(network=2)) .filter(Q(project_projectmember__member=self.request.user) | Q(network=2))
.select_related("workspace", "workspace__owner") .select_related(
"workspace", "workspace__owner", "default_assignee", "project_lead"
)
.distinct() .distinct()
) )
@ -294,7 +296,7 @@ class UserProjectInvitationsViewset(BaseViewSet):
super() super()
.get_queryset() .get_queryset()
.filter(email=self.request.user.email) .filter(email=self.request.user.email)
.select_related("workspace") .select_related("workspace", "workspace__owner", "project")
) )
def create(self, request): def create(self, request):
@ -349,6 +351,7 @@ class ProjectMemberViewSet(BaseViewSet):
.filter(project_id=self.kwargs.get("project_id")) .filter(project_id=self.kwargs.get("project_id"))
.select_related("project") .select_related("project")
.select_related("member") .select_related("member")
.select_related("workspace", "workspace__owner")
) )
@ -481,6 +484,7 @@ class ProjectMemberInvitationsViewset(BaseViewSet):
.filter(workspace__slug=self.kwargs.get("slug")) .filter(workspace__slug=self.kwargs.get("slug"))
.filter(project_id=self.kwargs.get("project_id")) .filter(project_id=self.kwargs.get("project_id"))
.select_related("project") .select_related("project")
.select_related("workspace", "workspace__owner")
) )
@ -496,7 +500,12 @@ class ProjectMemberInviteDetailViewSet(BaseViewSet):
] ]
def get_queryset(self): def get_queryset(self):
return self.filter_queryset(super().get_queryset().select_related("project")) return self.filter_queryset(
super()
.get_queryset()
.select_related("project")
.select_related("workspace", "workspace__owner")
)
class ProjectIdentifierEndpoint(BaseAPIView): class ProjectIdentifierEndpoint(BaseAPIView):

View File

@ -176,7 +176,7 @@ class InviteWorkspaceEndpoint(BaseAPIView):
workspace_members = WorkspaceMember.objects.filter( workspace_members = WorkspaceMember.objects.filter(
workspace_id=workspace.id, workspace_id=workspace.id,
member__email__in=[email.get("email") for email in emails], member__email__in=[email.get("email") for email in emails],
) ).select_related("member", "worspace", "workspace__owner")
if len(workspace_members): if len(workspace_members):
return Response( return Response(
@ -339,7 +339,7 @@ class WorkspaceInvitationsViewset(BaseViewSet):
super() super()
.get_queryset() .get_queryset()
.filter(workspace__slug=self.kwargs.get("slug")) .filter(workspace__slug=self.kwargs.get("slug"))
.select_related("workspace") .select_related("workspace", "workspace__owner")
) )
@ -353,7 +353,7 @@ class UserWorkspaceInvitationsEndpoint(BaseViewSet):
super() super()
.get_queryset() .get_queryset()
.filter(email=self.request.user.email) .filter(email=self.request.user.email)
.select_related("workspace") .select_related("workspace", "workspace__owner")
) )
def create(self, request): def create(self, request):
@ -524,7 +524,7 @@ class UserLastProjectWithWorkspaceEndpoint(BaseAPIView):
project_member = ProjectMember.objects.filter( project_member = ProjectMember.objects.filter(
workspace_id=last_workspace_id, member=request.user workspace_id=last_workspace_id, member=request.user
).select_related("workspace", "project", "member") ).select_related("workspace", "project", "member", "workspace__owner")
project_member_serializer = ProjectMemberSerializer( project_member_serializer = ProjectMemberSerializer(
project_member, many=True project_member, many=True

View File

@ -14,7 +14,7 @@ from sentry_sdk.integrations.redis import RedisIntegration
from .common import * # noqa from .common import * # noqa
# Database # Database
DEBUG = False DEBUG = True
DATABASES = { DATABASES = {
"default": { "default": {
"ENGINE": "django.db.backends.postgresql_psycopg2", "ENGINE": "django.db.backends.postgresql_psycopg2",

View File

@ -6,7 +6,7 @@ django-taggit==2.1.0
psycopg2==2.9.3 psycopg2==2.9.3
django-oauth-toolkit==2.0.0 django-oauth-toolkit==2.0.0
mistune==2.0.3 mistune==2.0.3
djangorestframework==3.13.1 djangorestframework==3.14.0
redis==4.2.2 redis==4.2.2
django-nested-admin==3.4.0 django-nested-admin==3.4.0
django-cors-headers==3.11.0 django-cors-headers==3.11.0
@ -16,7 +16,7 @@ faker==13.4.0
django-filter==21.1 django-filter==21.1
jsonmodels==2.5.0 jsonmodels==2.5.0
djangorestframework-simplejwt==5.1.0 djangorestframework-simplejwt==5.1.0
sentry-sdk==1.5.12 sentry-sdk==1.13.0
django-s3-storage==0.13.6 django-s3-storage==0.13.6
django-crum==0.7.9 django-crum==0.7.9
django-guardian==2.4.0 django-guardian==2.4.0