From 7e0520d1cf0ca0d3c675d86d80c8b2a2ff712bb9 Mon Sep 17 00:00:00 2001 From: Nikhil <118773738+pablohashescobar@users.noreply.github.com> Date: Tue, 9 Apr 2024 13:36:08 +0530 Subject: [PATCH] [WEB - 922]dev: fix workspace member caching (#4147) * dev: fix workspace member caching * fix: caching on debug --- apiserver/plane/app/views/workspace/invite.py | 34 ++++++----- apiserver/plane/app/views/workspace/member.py | 21 ++++++- apiserver/plane/utils/cache.py | 56 +++++++++++-------- 3 files changed, 70 insertions(+), 41 deletions(-) diff --git a/apiserver/plane/app/views/workspace/invite.py b/apiserver/plane/app/views/workspace/invite.py index 807c060ad..0b6f03e8a 100644 --- a/apiserver/plane/app/views/workspace/invite.py +++ b/apiserver/plane/app/views/workspace/invite.py @@ -1,36 +1,39 @@ # Python imports -import jwt from datetime import datetime +import jwt + # Django imports from django.conf import settings -from django.utils import timezone -from django.db.models import Count from django.core.exceptions import ValidationError from django.core.validators import validate_email +from django.db.models import Count +from django.utils import timezone # Third party modules from rest_framework import status -from rest_framework.response import Response from rest_framework.permissions import AllowAny +from rest_framework.response import Response # Module imports +from plane.app.permissions import WorkSpaceAdminPermission from plane.app.serializers import ( - WorkSpaceMemberSerializer, WorkSpaceMemberInviteSerializer, + WorkSpaceMemberSerializer, ) from plane.app.views.base import BaseAPIView -from .. import BaseViewSet +from plane.bgtasks.event_tracking_task import workspace_invite_event +from plane.bgtasks.workspace_invitation_task import workspace_invitation from plane.db.models import ( User, Workspace, - WorkspaceMemberInvite, WorkspaceMember, + WorkspaceMemberInvite, ) -from plane.app.permissions import WorkSpaceAdminPermission -from plane.bgtasks.workspace_invitation_task import workspace_invitation -from plane.bgtasks.event_tracking_task import workspace_invite_event -from plane.utils.cache import invalidate_cache +from plane.utils.cache import invalidate_cache, invalidate_cache_directly + +from .. import BaseViewSet + class WorkspaceInvitationsViewset(BaseViewSet): """Endpoint for creating, listing and deleting workspaces""" @@ -265,9 +268,6 @@ class UserWorkspaceInvitationsViewSet(BaseViewSet): @invalidate_cache(path="/api/workspaces/", user=False) @invalidate_cache(path="/api/users/me/workspaces/") - @invalidate_cache( - path="/api/workspaces/:slug/members/", url_params=True, user=False - ) def create(self, request): invitations = request.data.get("invitations", []) workspace_invitations = WorkspaceMemberInvite.objects.filter( @@ -276,6 +276,12 @@ class UserWorkspaceInvitationsViewSet(BaseViewSet): # If the user is already a member of workspace and was deactivated then activate the user for invitation in workspace_invitations: + invalidate_cache_directly( + path=f"/api/workspaces/{invitation.workspace.slug}/members/", + user=False, + request=request, + multiple=True, + ) # Update the WorkspaceMember for this specific invitation WorkspaceMember.objects.filter( workspace_id=invitation.workspace_id, member=request.user diff --git a/apiserver/plane/app/views/workspace/member.py b/apiserver/plane/app/views/workspace/member.py index 5afe37144..6ea2b3f20 100644 --- a/apiserver/plane/app/views/workspace/member.py +++ b/apiserver/plane/app/views/workspace/member.py @@ -102,7 +102,10 @@ class WorkSpaceMemberViewSet(BaseViewSet): return Response(serializer.data, status=status.HTTP_200_OK) @invalidate_cache( - path="/api/workspaces/:slug/members/", url_params=True, user=False + path="/api/workspaces/:slug/members/", + url_params=True, + user=False, + multiple=True, ) def partial_update(self, request, slug, pk): workspace_member = WorkspaceMember.objects.get( @@ -147,9 +150,15 @@ class WorkSpaceMemberViewSet(BaseViewSet): return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) @invalidate_cache( - path="/api/workspaces/:slug/members/", url_params=True, user=False + path="/api/workspaces/:slug/members/", + url_params=True, + user=False, + multiple=True, ) @invalidate_cache(path="/api/users/me/settings/") + @invalidate_cache( + path="/api/users/me/workspaces/", user=False, multiple=True + ) def destroy(self, request, slug, pk): # Check the user role who is deleting the user workspace_member = WorkspaceMember.objects.get( @@ -215,9 +224,15 @@ class WorkSpaceMemberViewSet(BaseViewSet): return Response(status=status.HTTP_204_NO_CONTENT) @invalidate_cache( - path="/api/workspaces/:slug/members/", url_params=True, user=False + path="/api/workspaces/:slug/members/", + url_params=True, + user=False, + multiple=True, ) @invalidate_cache(path="/api/users/me/settings/") + @invalidate_cache( + path="api/users/me/workspaces/", user=False, multiple=True + ) def leave(self, request, slug): workspace_member = WorkspaceMember.objects.get( workspace__slug=slug, diff --git a/apiserver/plane/utils/cache.py b/apiserver/plane/utils/cache.py index aece1d644..071051129 100644 --- a/apiserver/plane/utils/cache.py +++ b/apiserver/plane/utils/cache.py @@ -33,12 +33,12 @@ def cache_response(timeout=60 * 60, path=None, user=True): custom_path = path if path is not None else request.get_full_path() key = generate_cache_key(custom_path, auth_header) cached_result = cache.get(key) + if cached_result is not None: return Response( cached_result["data"], status=cached_result["status"] ) response = view_func(instance, request, *args, **kwargs) - if response.status_code == 200 and not settings.DEBUG: cache.set( key, @@ -53,34 +53,42 @@ def cache_response(timeout=60 * 60, path=None, user=True): return decorator -def invalidate_cache(path=None, url_params=False, user=True): - """invalidate cache per user""" +def invalidate_cache_directly( + path=None, url_params=False, user=True, request=None, multiple=False +): + if url_params and path: + path_with_values = path + # Assuming `kwargs` could be passed directly if needed, otherwise, skip this part + for key, value in request.resolver_match.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() + auth_header = ( + None + if request.user.is_anonymous + else str(request.user.id) if user else None + ) + key = generate_cache_key(custom_path, auth_header) + if multiple: + cache.delete_many(keys=cache.keys(f"*{key}*")) + else: + cache.delete(key) + + +def invalidate_cache(path=None, url_params=False, user=True, multiple=False): def decorator(view_func): @wraps(view_func) def _wrapped_view(instance, request, *args, **kwargs): - # Invalidate cache before executing the view function - if 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() - ) - - auth_header = ( - None - if request.user.is_anonymous - else str(request.user.id) if user else None + # invalidate the cache + invalidate_cache_directly( + path=path, + url_params=url_params, + user=user, + request=request, + multiple=multiple, ) - key = generate_cache_key(custom_path, auth_header) - cache.delete(key) - # Execute the view function return view_func(instance, request, *args, **kwargs) return _wrapped_view