dev: cache apis for workspace, projects and issues.

This commit is contained in:
pablohashescobar 2024-02-16 16:18:52 +05:30
parent e4bad543a4
commit 5b09083f93
9 changed files with 58 additions and 15 deletions

View File

@ -78,6 +78,7 @@ class CycleSerializer(BaseSerializer):
"workspace_id", "workspace_id",
"project_id", "project_id",
# model fields # model fields
"name",
"description", "description",
"start_date", "start_date",
"end_date", "end_date",

View File

@ -18,7 +18,7 @@ from plane.db.models import (
class WorkSpaceSerializer(DynamicBaseSerializer): class WorkSpaceSerializer(DynamicBaseSerializer):
owner_id = serializers.PrimaryKeyRelatedField(read_only=True) owner = UserLiteSerializer(read_only=True)
total_members = serializers.IntegerField(read_only=True) total_members = serializers.IntegerField(read_only=True)
total_issues = serializers.IntegerField(read_only=True) total_issues = serializers.IntegerField(read_only=True)
@ -48,7 +48,7 @@ class WorkSpaceSerializer(DynamicBaseSerializer):
"updated_by", "updated_by",
"created_at", "created_at",
"updated_at", "updated_at",
"owner_id", "owner",
] ]

View File

@ -11,13 +11,14 @@ from plane.app.serializers import (
EstimatePointSerializer, EstimatePointSerializer,
EstimateReadSerializer, EstimateReadSerializer,
) )
from plane.utils.cache import cache_path_response, invalidate_path_cache
class ProjectEstimatePointEndpoint(BaseAPIView): class ProjectEstimatePointEndpoint(BaseAPIView):
permission_classes = [ permission_classes = [
ProjectEntityPermission, ProjectEntityPermission,
] ]
@cache_path_response(60 * 60 * 2)
def get(self, request, slug, project_id): def get(self, request, slug, project_id):
project = Project.objects.get(workspace__slug=slug, pk=project_id) project = Project.objects.get(workspace__slug=slug, pk=project_id)
if project.estimate_id is not None: if project.estimate_id is not None:
@ -38,6 +39,7 @@ class BulkEstimatePointEndpoint(BaseViewSet):
model = Estimate model = Estimate
serializer_class = EstimateSerializer serializer_class = EstimateSerializer
@cache_path_response(60 * 60 * 2)
def list(self, request, slug, project_id): def list(self, request, slug, project_id):
estimates = ( estimates = (
Estimate.objects.filter( Estimate.objects.filter(
@ -49,6 +51,9 @@ class BulkEstimatePointEndpoint(BaseViewSet):
serializer = EstimateReadSerializer(estimates, many=True) serializer = EstimateReadSerializer(estimates, many=True)
return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.data, status=status.HTTP_200_OK)
@invalidate_path_cache("/api/workspaces/:slug/estimates/", True)
@invalidate_path_cache("/api/workspaces/:slug/projects/:project_id/estimates/", True)
@invalidate_path_cache("/api/workspaces/:slug/projects/:project_id/project-estimates/", True)
def create(self, request, slug, project_id): def create(self, request, slug, project_id):
if not request.data.get("estimate", False): if not request.data.get("estimate", False):
return Response( return Response(
@ -114,6 +119,9 @@ class BulkEstimatePointEndpoint(BaseViewSet):
status=status.HTTP_200_OK, status=status.HTTP_200_OK,
) )
@invalidate_path_cache("/api/workspaces/:slug/estimates/", True)
@invalidate_path_cache("/api/workspaces/:slug/projects/:project_id/estimates/", True)
@invalidate_path_cache("/api/workspaces/:slug/projects/:project_id/project-estimates/", True)
def partial_update(self, request, slug, project_id, estimate_id): def partial_update(self, request, slug, project_id, estimate_id):
if not request.data.get("estimate", False): if not request.data.get("estimate", False):
return Response( return Response(

View File

@ -81,7 +81,7 @@ from plane.bgtasks.issue_activites_task import issue_activity
from plane.utils.grouper import group_results from plane.utils.grouper import group_results
from plane.utils.issue_filters import issue_filters from plane.utils.issue_filters import issue_filters
from collections import defaultdict from collections import defaultdict
from plane.utils.cache import cache_path_response, invalidate_path_cache
class IssueViewSet(WebhookMixin, BaseViewSet): class IssueViewSet(WebhookMixin, BaseViewSet):
def get_serializer_class(self): def get_serializer_class(self):
@ -1111,6 +1111,7 @@ class IssueArchiveViewSet(BaseViewSet):
) )
@method_decorator(gzip_page) @method_decorator(gzip_page)
@cache_path_response(60 * 60 * 3)
def list(self, request, slug, project_id): def list(self, request, slug, project_id):
fields = [ fields = [
field field
@ -1217,6 +1218,7 @@ class IssueArchiveViewSet(BaseViewSet):
) )
return Response(IssueSerializer(issue).data, status=status.HTTP_200_OK) return Response(IssueSerializer(issue).data, status=status.HTTP_200_OK)
@invalidate_path_cache("/api/workspaces/:slug/projects/:project_id/archived-issues/", True)
def unarchive(self, request, slug, project_id, pk=None): def unarchive(self, request, slug, project_id, pk=None):
issue = Issue.objects.get( issue = Issue.objects.get(
workspace__slug=slug, workspace__slug=slug,

View File

@ -65,7 +65,7 @@ from plane.db.models import (
) )
from plane.bgtasks.project_invitation_task import project_invitation from plane.bgtasks.project_invitation_task import project_invitation
from plane.utils.cache import cache_path_response, invalidate_path_cache
class ProjectViewSet(WebhookMixin, BaseViewSet): class ProjectViewSet(WebhookMixin, BaseViewSet):
serializer_class = ProjectListSerializer serializer_class = ProjectListSerializer
@ -662,6 +662,7 @@ class ProjectMemberViewSet(BaseViewSet):
.select_related("workspace", "workspace__owner") .select_related("workspace", "workspace__owner")
) )
@invalidate_path_cache("/api/workspaces/:slug/projects/:project_id/members/", True)
def create(self, request, slug, project_id): def create(self, request, slug, project_id):
members = request.data.get("members", []) members = request.data.get("members", [])
@ -738,6 +739,7 @@ class ProjectMemberViewSet(BaseViewSet):
serializer = ProjectMemberRoleSerializer(project_members, many=True) serializer = ProjectMemberRoleSerializer(project_members, many=True)
return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.data, status=status.HTTP_201_CREATED)
@cache_path_response(60 * 60 * 2)
def list(self, request, slug, project_id): def list(self, request, slug, project_id):
# Get the list of project members for the project # Get the list of project members for the project
project_members = ProjectMember.objects.filter( project_members = ProjectMember.objects.filter(
@ -752,6 +754,7 @@ class ProjectMemberViewSet(BaseViewSet):
) )
return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.data, status=status.HTTP_200_OK)
@invalidate_path_cache("/api/workspaces/:slug/projects/:project_id/members/", True)
def partial_update(self, request, slug, project_id, pk): def partial_update(self, request, slug, project_id, pk):
project_member = ProjectMember.objects.get( project_member = ProjectMember.objects.get(
pk=pk, pk=pk,
@ -792,6 +795,7 @@ class ProjectMemberViewSet(BaseViewSet):
return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@invalidate_path_cache("/api/workspaces/:slug/projects/:project_id/members/", True)
def destroy(self, request, slug, project_id, pk): def destroy(self, request, slug, project_id, pk):
project_member = ProjectMember.objects.get( project_member = ProjectMember.objects.get(
workspace__slug=slug, workspace__slug=slug,

View File

@ -16,7 +16,7 @@ from plane.app.permissions import (
WorkspaceEntityPermission, WorkspaceEntityPermission,
) )
from plane.db.models import State, Issue from plane.db.models import State, Issue
from plane.utils.cache import cache_path_response, invalidate_path_cache
class StateViewSet(BaseViewSet): class StateViewSet(BaseViewSet):
serializer_class = StateSerializer serializer_class = StateSerializer
@ -38,6 +38,8 @@ class StateViewSet(BaseViewSet):
.distinct() .distinct()
) )
@invalidate_path_cache()
@invalidate_path_cache("workspaces/:slug/states/", True)
def create(self, request, slug, project_id): def create(self, request, slug, project_id):
serializer = StateSerializer(data=request.data) serializer = StateSerializer(data=request.data)
if serializer.is_valid(): if serializer.is_valid():
@ -45,6 +47,7 @@ class StateViewSet(BaseViewSet):
return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@cache_path_response(60 * 60 * 1)
def list(self, request, slug, project_id): def list(self, request, slug, project_id):
states = StateSerializer(self.get_queryset(), many=True).data states = StateSerializer(self.get_queryset(), many=True).data
grouped = request.GET.get("grouped", False) grouped = request.GET.get("grouped", False)
@ -58,6 +61,8 @@ class StateViewSet(BaseViewSet):
return Response(state_dict, status=status.HTTP_200_OK) return Response(state_dict, status=status.HTTP_200_OK)
return Response(states, status=status.HTTP_200_OK) return Response(states, status=status.HTTP_200_OK)
@invalidate_path_cache("/api/workspaces/:slug/projects/:project_id/states/", True)
@invalidate_path_cache("workspaces/:slug/states/", True)
def mark_as_default(self, request, slug, project_id, pk): def mark_as_default(self, request, slug, project_id, pk):
# Select all the states which are marked as default # Select all the states which are marked as default
_ = State.objects.filter( _ = State.objects.filter(
@ -68,6 +73,8 @@ class StateViewSet(BaseViewSet):
).update(default=True) ).update(default=True)
return Response(status=status.HTTP_204_NO_CONTENT) return Response(status=status.HTTP_204_NO_CONTENT)
@invalidate_path_cache("/api/workspaces/:slug/projects/:project_id/states/", True)
@invalidate_path_cache("workspaces/:slug/states/", True)
def destroy(self, request, slug, project_id, pk): def destroy(self, request, slug, project_id, pk):
state = State.objects.get( state = State.objects.get(
~Q(name="Triage"), ~Q(name="Triage"),

View File

@ -83,6 +83,11 @@ from plane.app.permissions import (
from plane.bgtasks.workspace_invitation_task import workspace_invitation from plane.bgtasks.workspace_invitation_task import workspace_invitation
from plane.utils.issue_filters import issue_filters from plane.utils.issue_filters import issue_filters
from plane.bgtasks.event_tracking_task import workspace_invite_event from plane.bgtasks.event_tracking_task import workspace_invite_event
from plane.utils.cache import (
cache_path_response,
invalidate_path_cache,
cache_user_response,
)
class WorkSpaceViewSet(BaseViewSet): class WorkSpaceViewSet(BaseViewSet):
@ -542,6 +547,7 @@ class WorkSpaceMemberViewSet(BaseViewSet):
.select_related("member") .select_related("member")
) )
@cache_path_response(60 * 5)
def list(self, request, slug): def list(self, request, slug):
workspace_member = WorkspaceMember.objects.get( workspace_member = WorkspaceMember.objects.get(
member=request.user, member=request.user,
@ -566,6 +572,7 @@ class WorkSpaceMemberViewSet(BaseViewSet):
) )
return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.data, status=status.HTTP_200_OK)
@invalidate_path_cache("/api/workspaces/:slug/members/", True)
def partial_update(self, request, slug, pk): def partial_update(self, request, slug, pk):
workspace_member = WorkspaceMember.objects.get( workspace_member = WorkspaceMember.objects.get(
pk=pk, pk=pk,
@ -608,6 +615,7 @@ class WorkSpaceMemberViewSet(BaseViewSet):
return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@invalidate_path_cache("/api/workspaces/:slug/members/", True)
def destroy(self, request, slug, pk): def destroy(self, request, slug, pk):
# Check the user role who is deleting the user # Check the user role who is deleting the user
workspace_member = WorkspaceMember.objects.get( workspace_member = WorkspaceMember.objects.get(
@ -672,6 +680,7 @@ class WorkSpaceMemberViewSet(BaseViewSet):
workspace_member.save() workspace_member.save()
return Response(status=status.HTTP_204_NO_CONTENT) return Response(status=status.HTTP_204_NO_CONTENT)
@invalidate_path_cache("/api/workspaces/:slug/members/", True)
def leave(self, request, slug): def leave(self, request, slug):
workspace_member = WorkspaceMember.objects.get( workspace_member = WorkspaceMember.objects.get(
workspace__slug=slug, workspace__slug=slug,
@ -868,6 +877,8 @@ class UserLastProjectWithWorkspaceEndpoint(BaseAPIView):
class WorkspaceMemberUserEndpoint(BaseAPIView): class WorkspaceMemberUserEndpoint(BaseAPIView):
@cache_user_response(60 * 60)
def get(self, request, slug): def get(self, request, slug):
workspace_member = WorkspaceMember.objects.get( workspace_member = WorkspaceMember.objects.get(
member=request.user, member=request.user,
@ -1433,6 +1444,7 @@ class WorkspaceLabelsEndpoint(BaseAPIView):
WorkspaceViewerPermission, WorkspaceViewerPermission,
] ]
@cache_path_response(60 * 60 * 1)
def get(self, request, slug): def get(self, request, slug):
labels = Label.objects.filter( labels = Label.objects.filter(
workspace__slug=slug, workspace__slug=slug,
@ -1447,6 +1459,7 @@ class WorkspaceStatesEndpoint(BaseAPIView):
WorkspaceEntityPermission, WorkspaceEntityPermission,
] ]
@cache_path_response(60 * 60 * 1)
def get(self, request, slug): def get(self, request, slug):
states = State.objects.filter( states = State.objects.filter(
workspace__slug=slug, workspace__slug=slug,
@ -1461,6 +1474,7 @@ class WorkspaceEstimatesEndpoint(BaseAPIView):
WorkspaceEntityPermission, WorkspaceEntityPermission,
] ]
@cache_path_response(60 * 60 * 1)
def get(self, request, slug): def get(self, request, slug):
estimate_ids = Project.objects.filter( estimate_ids = Project.objects.filter(
workspace__slug=slug, estimate__isnull=False workspace__slug=slug, estimate__isnull=False

View File

@ -14,7 +14,11 @@ EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
CACHES = { CACHES = {
"default": { "default": {
"BACKEND": "django.core.cache.backends.locmem.LocMemCache", "BACKEND": "django_redis.cache.RedisCache",
"LOCATION": REDIS_URL,
"OPTIONS": {
"CLIENT_CLASS": "django_redis.client.DefaultClient",
},
} }
} }

View File

@ -30,9 +30,6 @@ def cache_user_response(timeout, path=None):
if response.status_code == 200: if response.status_code == 200:
cache.set(key, {'data': response.data, 'status': response.status_code}, timeout) cache.set(key, {'data': response.data, 'status': response.status_code}, timeout)
response['Cache-Control'] = f'max-age={timeout}'
expires_time = datetime.utcnow() + timedelta(seconds=timeout)
response['Expires'] = http_date(expires_time.timestamp())
return response return response
return _wrapped_view return _wrapped_view
@ -59,6 +56,7 @@ def cache_path_response(timeout, path=None):
@wraps(view_func) @wraps(view_func)
def _wrapped_view(instance, request, *args, **kwargs): def _wrapped_view(instance, request, *args, **kwargs):
# Function to generate cache key # Function to generate cache key
print(request.get_full_path())
custom_path = path if path is not None else request.get_full_path() custom_path = path if path is not None else request.get_full_path()
key = generate_cache_key(custom_path, None) key = generate_cache_key(custom_path, None)
cached_result = cache.get(key) cached_result = cache.get(key)
@ -69,20 +67,25 @@ def cache_path_response(timeout, path=None):
if response.status_code == 200: if response.status_code == 200:
cache.set(key, {'data': response.data, 'status': response.status_code}, timeout) cache.set(key, {'data': response.data, 'status': response.status_code}, timeout)
response['Cache-Control'] = f'max-age={timeout}'
expires_time = datetime.utcnow() + timedelta(seconds=timeout)
response['Expires'] = http_date(expires_time.timestamp())
return response return response
return _wrapped_view return _wrapped_view
return decorator return decorator
def invalidate_path_cache(path=None): def invalidate_path_cache(path=None, include_url_params=False):
def decorator(view_func): def decorator(view_func):
@wraps(view_func) @wraps(view_func)
def _wrapped_view(instance, request, *args, **kwargs): def _wrapped_view(instance, request, *args, **kwargs):
# Invalidate cache before executing the view function # Invalidate cache before executing the view function
custom_path = path if path is not None else request.get_full_path() if include_url_params:
path_with_values = path
for key, value in kwargs.items():
path_with_values = path_with_values.replace(f":{key}", str(value))
custom_path = path_with_values
else:
custom_path = path if path is not None else request.get_full_path()
key = generate_cache_key(custom_path, None) key = generate_cache_key(custom_path, None)
cache.delete(key) cache.delete(key)