mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
dev: cache apis for workspace, projects and issues.
This commit is contained in:
parent
e4bad543a4
commit
5b09083f93
@ -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",
|
||||||
|
@ -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",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -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(
|
||||||
|
@ -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,
|
||||||
|
@ -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,
|
||||||
|
@ -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"),
|
||||||
|
@ -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
|
||||||
|
@ -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",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user