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

View File

@ -2,7 +2,15 @@
from rest_framework.permissions import BasePermission, SAFE_METHODS
# 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
@ -22,13 +30,15 @@ class WorkSpaceBasePermission(BasePermission):
# allow only admins and owners to update the workspace settings
if request.method in ["PUT", "PATCH"]:
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()
# allow only owner to delete the workspace
if request.method == "DELETE":
return WorkspaceMember.objects.filter(
member=request.user, workspace=view.workspace, role=20
member=request.user, workspace__slug=view.workspace_slug, role=Owner
).exists()
@ -39,5 +49,7 @@ class WorkSpaceAdminPermission(BasePermission):
return False
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()

View File

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

View File

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

View File

@ -1,6 +1,7 @@
# Django imports
from django.urls import resolve
from django.conf import settings
# Third part imports
from rest_framework import status
from rest_framework.viewsets import ModelViewSet
@ -39,32 +40,23 @@ class BaseViewSet(ModelViewSet, BasePaginator):
return self.model.objects.all()
except Exception as e:
print(e)
raise APIException(
"Please check the view", status.HTTP_400_BAD_REQUEST
)
raise APIException("Please check the view", status.HTTP_400_BAD_REQUEST)
def dispatch(self, request, *args, **kwargs):
response = super().dispatch(request, *args, **kwargs)
if settings.DEBUG:
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
@property
def workspace_slug(self):
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
def project_id(self):
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":
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):
@ -110,33 +92,16 @@ class BaseAPIView(APIView, BasePaginator):
if settings.DEBUG:
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
@property
def workspace_slug(self):
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
def project_id(self):
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)
.select_related("project")
.select_related("workspace")
.select_related("owned_by")
.distinct()
)
@ -62,8 +63,8 @@ class CycleIssueViewSet(BaseViewSet):
.select_related("project")
.select_related("workspace")
.select_related("cycle")
.select_related("issue")
.select_related("issue__state")
.select_related("issue", "issue__state", "issue__project")
.prefetch_related("issue__assignees", "issue__labels")
.distinct()
)

View File

@ -125,7 +125,9 @@ class IssueViewSet(BaseViewSet):
.prefetch_related(
Prefetch(
"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 self.paginate(
request=request,
queryset=issue_queryset,
on_results=lambda issues: IssueSerializer(issues, many=True).data,
return Response(
{
"next_cursor": str(0),
"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:
@ -206,8 +216,42 @@ class IssueViewSet(BaseViewSet):
class UserWorkSpaceIssues(BaseAPIView):
def get(self, request, slug):
try:
issues = Issue.objects.filter(
assignees__in=[request.user], workspace__slug=slug
issues = (
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)
return Response(serializer.data, status=status.HTTP_200_OK)

View File

@ -15,7 +15,13 @@ from plane.api.serializers import (
ModuleIssueSerializer,
)
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):
@ -45,13 +51,15 @@ class ModuleViewSet(BaseViewSet):
.prefetch_related(
Prefetch(
"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(
"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("workspace")
.select_related("module")
.select_related("issue")
.select_related("issue", "issue__state", "issue__project")
.prefetch_related("issue__assignees", "issue__labels")
.prefetch_related("module__members")
.distinct()
)
@ -164,4 +174,4 @@ class ModuleIssueViewSet(BaseViewSet):
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
)

View File

@ -67,7 +67,9 @@ class ProjectViewSet(BaseViewSet):
.get_queryset()
.filter(workspace__slug=self.kwargs.get("slug"))
.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()
)
@ -294,7 +296,7 @@ class UserProjectInvitationsViewset(BaseViewSet):
super()
.get_queryset()
.filter(email=self.request.user.email)
.select_related("workspace")
.select_related("workspace", "workspace__owner", "project")
)
def create(self, request):
@ -349,6 +351,7 @@ class ProjectMemberViewSet(BaseViewSet):
.filter(project_id=self.kwargs.get("project_id"))
.select_related("project")
.select_related("member")
.select_related("workspace", "workspace__owner")
)
@ -481,6 +484,7 @@ class ProjectMemberInvitationsViewset(BaseViewSet):
.filter(workspace__slug=self.kwargs.get("slug"))
.filter(project_id=self.kwargs.get("project_id"))
.select_related("project")
.select_related("workspace", "workspace__owner")
)
@ -496,7 +500,12 @@ class ProjectMemberInviteDetailViewSet(BaseViewSet):
]
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):

View File

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

View File

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

View File

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