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",
"project_id",
# model fields
"name",
"description",
"start_date",
"end_date",

View File

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

View File

@ -11,13 +11,14 @@ from plane.app.serializers import (
EstimatePointSerializer,
EstimateReadSerializer,
)
from plane.utils.cache import cache_path_response, invalidate_path_cache
class ProjectEstimatePointEndpoint(BaseAPIView):
permission_classes = [
ProjectEntityPermission,
]
@cache_path_response(60 * 60 * 2)
def get(self, request, slug, project_id):
project = Project.objects.get(workspace__slug=slug, pk=project_id)
if project.estimate_id is not None:
@ -38,6 +39,7 @@ class BulkEstimatePointEndpoint(BaseViewSet):
model = Estimate
serializer_class = EstimateSerializer
@cache_path_response(60 * 60 * 2)
def list(self, request, slug, project_id):
estimates = (
Estimate.objects.filter(
@ -49,6 +51,9 @@ class BulkEstimatePointEndpoint(BaseViewSet):
serializer = EstimateReadSerializer(estimates, many=True)
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):
if not request.data.get("estimate", False):
return Response(
@ -114,6 +119,9 @@ class BulkEstimatePointEndpoint(BaseViewSet):
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):
if not request.data.get("estimate", False):
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.issue_filters import issue_filters
from collections import defaultdict
from plane.utils.cache import cache_path_response, invalidate_path_cache
class IssueViewSet(WebhookMixin, BaseViewSet):
def get_serializer_class(self):
@ -1111,6 +1111,7 @@ class IssueArchiveViewSet(BaseViewSet):
)
@method_decorator(gzip_page)
@cache_path_response(60 * 60 * 3)
def list(self, request, slug, project_id):
fields = [
field
@ -1217,6 +1218,7 @@ class IssueArchiveViewSet(BaseViewSet):
)
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):
issue = Issue.objects.get(
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.utils.cache import cache_path_response, invalidate_path_cache
class ProjectViewSet(WebhookMixin, BaseViewSet):
serializer_class = ProjectListSerializer
@ -662,6 +662,7 @@ class ProjectMemberViewSet(BaseViewSet):
.select_related("workspace", "workspace__owner")
)
@invalidate_path_cache("/api/workspaces/:slug/projects/:project_id/members/", True)
def create(self, request, slug, project_id):
members = request.data.get("members", [])
@ -738,6 +739,7 @@ class ProjectMemberViewSet(BaseViewSet):
serializer = ProjectMemberRoleSerializer(project_members, many=True)
return Response(serializer.data, status=status.HTTP_201_CREATED)
@cache_path_response(60 * 60 * 2)
def list(self, request, slug, project_id):
# Get the list of project members for the project
project_members = ProjectMember.objects.filter(
@ -752,6 +754,7 @@ class ProjectMemberViewSet(BaseViewSet):
)
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):
project_member = ProjectMember.objects.get(
pk=pk,
@ -792,6 +795,7 @@ class ProjectMemberViewSet(BaseViewSet):
return Response(serializer.data, status=status.HTTP_200_OK)
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):
project_member = ProjectMember.objects.get(
workspace__slug=slug,

View File

@ -16,7 +16,7 @@ from plane.app.permissions import (
WorkspaceEntityPermission,
)
from plane.db.models import State, Issue
from plane.utils.cache import cache_path_response, invalidate_path_cache
class StateViewSet(BaseViewSet):
serializer_class = StateSerializer
@ -38,6 +38,8 @@ class StateViewSet(BaseViewSet):
.distinct()
)
@invalidate_path_cache()
@invalidate_path_cache("workspaces/:slug/states/", True)
def create(self, request, slug, project_id):
serializer = StateSerializer(data=request.data)
if serializer.is_valid():
@ -45,6 +47,7 @@ class StateViewSet(BaseViewSet):
return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@cache_path_response(60 * 60 * 1)
def list(self, request, slug, project_id):
states = StateSerializer(self.get_queryset(), many=True).data
grouped = request.GET.get("grouped", False)
@ -58,6 +61,8 @@ class StateViewSet(BaseViewSet):
return Response(state_dict, 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):
# Select all the states which are marked as default
_ = State.objects.filter(
@ -68,6 +73,8 @@ class StateViewSet(BaseViewSet):
).update(default=True)
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):
state = State.objects.get(
~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.utils.issue_filters import issue_filters
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):
@ -542,6 +547,7 @@ class WorkSpaceMemberViewSet(BaseViewSet):
.select_related("member")
)
@cache_path_response(60 * 5)
def list(self, request, slug):
workspace_member = WorkspaceMember.objects.get(
member=request.user,
@ -566,6 +572,7 @@ class WorkSpaceMemberViewSet(BaseViewSet):
)
return Response(serializer.data, status=status.HTTP_200_OK)
@invalidate_path_cache("/api/workspaces/:slug/members/", True)
def partial_update(self, request, slug, pk):
workspace_member = WorkspaceMember.objects.get(
pk=pk,
@ -608,6 +615,7 @@ class WorkSpaceMemberViewSet(BaseViewSet):
return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@invalidate_path_cache("/api/workspaces/:slug/members/", True)
def destroy(self, request, slug, pk):
# Check the user role who is deleting the user
workspace_member = WorkspaceMember.objects.get(
@ -672,6 +680,7 @@ class WorkSpaceMemberViewSet(BaseViewSet):
workspace_member.save()
return Response(status=status.HTTP_204_NO_CONTENT)
@invalidate_path_cache("/api/workspaces/:slug/members/", True)
def leave(self, request, slug):
workspace_member = WorkspaceMember.objects.get(
workspace__slug=slug,
@ -868,6 +877,8 @@ class UserLastProjectWithWorkspaceEndpoint(BaseAPIView):
class WorkspaceMemberUserEndpoint(BaseAPIView):
@cache_user_response(60 * 60)
def get(self, request, slug):
workspace_member = WorkspaceMember.objects.get(
member=request.user,
@ -1433,6 +1444,7 @@ class WorkspaceLabelsEndpoint(BaseAPIView):
WorkspaceViewerPermission,
]
@cache_path_response(60 * 60 * 1)
def get(self, request, slug):
labels = Label.objects.filter(
workspace__slug=slug,
@ -1447,6 +1459,7 @@ class WorkspaceStatesEndpoint(BaseAPIView):
WorkspaceEntityPermission,
]
@cache_path_response(60 * 60 * 1)
def get(self, request, slug):
states = State.objects.filter(
workspace__slug=slug,
@ -1461,6 +1474,7 @@ class WorkspaceEstimatesEndpoint(BaseAPIView):
WorkspaceEntityPermission,
]
@cache_path_response(60 * 60 * 1)
def get(self, request, slug):
estimate_ids = Project.objects.filter(
workspace__slug=slug, estimate__isnull=False

View File

@ -14,7 +14,11 @@ EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
CACHES = {
"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:
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 _wrapped_view
@ -59,6 +56,7 @@ def cache_path_response(timeout, path=None):
@wraps(view_func)
def _wrapped_view(instance, request, *args, **kwargs):
# Function to generate cache key
print(request.get_full_path())
custom_path = path if path is not None else request.get_full_path()
key = generate_cache_key(custom_path, None)
cached_result = cache.get(key)
@ -69,20 +67,25 @@ def cache_path_response(timeout, path=None):
if response.status_code == 200:
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 _wrapped_view
return decorator
def invalidate_path_cache(path=None):
def invalidate_path_cache(path=None, include_url_params=False):
def decorator(view_func):
@wraps(view_func)
def _wrapped_view(instance, request, *args, **kwargs):
# 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)
cache.delete(key)