Skip to content

Commit cea6f75

Browse files
authored
[SILO-671] feat: add sticky external apis (#8139)
* add sticky external apis * add created_at sort by to list * remove select related method from query set
1 parent a7e2e59 commit cea6f75

File tree

9 files changed

+192
-2
lines changed

9 files changed

+192
-2
lines changed

apps/api/plane/api/serializers/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,4 +54,5 @@
5454
FileAssetSerializer,
5555
)
5656
from .invite import WorkspaceInviteSerializer
57-
from .member import ProjectMemberSerializer
57+
from .member import ProjectMemberSerializer
58+
from .sticky import StickySerializer
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
from rest_framework import serializers
2+
3+
from .base import BaseSerializer
4+
from plane.db.models import Sticky
5+
from plane.utils.content_validator import validate_html_content, validate_binary_data
6+
7+
8+
class StickySerializer(BaseSerializer):
9+
class Meta:
10+
model = Sticky
11+
fields = "__all__"
12+
read_only_fields = ["workspace", "owner"]
13+
extra_kwargs = {"name": {"required": False}}
14+
15+
def validate(self, data):
16+
# Validate description content for security
17+
if "description_html" in data and data["description_html"]:
18+
is_valid, error_msg, sanitized_html = validate_html_content(data["description_html"])
19+
if not is_valid:
20+
raise serializers.ValidationError({"error": "html content is not valid"})
21+
# Update the data with sanitized HTML if available
22+
if sanitized_html is not None:
23+
data["description_html"] = sanitized_html
24+
25+
if "description_binary" in data and data["description_binary"]:
26+
is_valid, error_msg = validate_binary_data(data["description_binary"])
27+
if not is_valid:
28+
raise serializers.ValidationError({"description_binary": "Invalid binary data"})
29+
30+
return data

apps/api/plane/api/urls/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from .user import urlpatterns as user_patterns
1010
from .work_item import urlpatterns as work_item_patterns
1111
from .invite import urlpatterns as invite_patterns
12+
from .sticky import urlpatterns as sticky_patterns
1213

1314
urlpatterns = [
1415
*asset_patterns,
@@ -22,4 +23,5 @@
2223
*user_patterns,
2324
*work_item_patterns,
2425
*invite_patterns,
26+
*sticky_patterns,
2527
]

apps/api/plane/api/urls/sticky.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
from django.urls import path, include
2+
from rest_framework.routers import DefaultRouter
3+
4+
from plane.api.views import StickyViewSet
5+
6+
7+
router = DefaultRouter()
8+
router.register(r"stickies", StickyViewSet, basename="workspace-stickies")
9+
10+
urlpatterns = [
11+
path("workspaces/<str:slug>/", include(router.urls)),
12+
]

apps/api/plane/api/views/__init__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,4 +54,6 @@
5454

5555
from .user import UserEndpoint
5656

57-
from .invite import WorkspaceInvitationsViewset
57+
from .invite import WorkspaceInvitationsViewset
58+
59+
from .sticky import StickyViewSet

apps/api/plane/api/views/sticky.py

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
from rest_framework.response import Response
2+
from rest_framework import status
3+
4+
from plane.api.views.base import BaseViewSet
5+
from plane.app.permissions import WorkspaceUserPermission
6+
from plane.db.models import Sticky, Workspace
7+
from plane.api.serializers import StickySerializer
8+
9+
# OpenAPI imports
10+
from plane.utils.openapi.decorators import sticky_docs
11+
12+
from drf_spectacular.utils import OpenApiRequest, OpenApiResponse
13+
from plane.utils.openapi import (
14+
STICKY_EXAMPLE,
15+
create_paginated_response,
16+
DELETED_RESPONSE,
17+
)
18+
19+
20+
class StickyViewSet(BaseViewSet):
21+
serializer_class = StickySerializer
22+
model = Sticky
23+
use_read_replica = True
24+
permission_classes = [WorkspaceUserPermission]
25+
26+
def get_queryset(self):
27+
return self.filter_queryset(
28+
super()
29+
.get_queryset()
30+
.filter(workspace__slug=self.kwargs.get("slug"))
31+
.filter(owner_id=self.request.user.id)
32+
.distinct()
33+
)
34+
35+
@sticky_docs(
36+
operation_id="create_sticky",
37+
summary="Create a new sticky",
38+
description="Create a new sticky in the workspace",
39+
request=OpenApiRequest(request=StickySerializer),
40+
responses={
41+
201: OpenApiResponse(description="Sticky created", response=StickySerializer, examples=[STICKY_EXAMPLE])
42+
},
43+
)
44+
def create(self, request, slug):
45+
workspace = Workspace.objects.get(slug=slug)
46+
serializer = StickySerializer(data=request.data)
47+
if serializer.is_valid():
48+
serializer.save(workspace_id=workspace.id, owner_id=request.user.id)
49+
return Response(serializer.data, status=status.HTTP_201_CREATED)
50+
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
51+
52+
@sticky_docs(
53+
operation_id="list_stickies",
54+
summary="List stickies",
55+
description="List all stickies in the workspace",
56+
responses={
57+
200: create_paginated_response(
58+
StickySerializer, "Sticky", "List of stickies", example_name="List of stickies"
59+
)
60+
},
61+
)
62+
def list(self, request, slug):
63+
query = request.query_params.get("query", False)
64+
stickies = self.get_queryset().order_by("-created_at")
65+
if query:
66+
stickies = stickies.filter(description_stripped__icontains=query)
67+
68+
return self.paginate(
69+
request=request,
70+
queryset=(stickies),
71+
on_results=lambda stickies: StickySerializer(stickies, many=True).data,
72+
default_per_page=20,
73+
)
74+
75+
@sticky_docs(
76+
operation_id="retrieve_sticky",
77+
summary="Retrieve a sticky",
78+
description="Retrieve a sticky by its ID",
79+
responses={200: OpenApiResponse(description="Sticky", response=StickySerializer, examples=[STICKY_EXAMPLE])},
80+
)
81+
def retrieve(self, request, slug, pk):
82+
sticky = self.get_object()
83+
return Response(StickySerializer(sticky).data)
84+
85+
@sticky_docs(
86+
operation_id="update_sticky",
87+
summary="Update a sticky",
88+
description="Update a sticky by its ID",
89+
request=OpenApiRequest(request=StickySerializer),
90+
responses={200: OpenApiResponse(description="Sticky", response=StickySerializer, examples=[STICKY_EXAMPLE])},
91+
)
92+
def partial_update(self, request, slug, pk):
93+
sticky = self.get_object()
94+
serializer = StickySerializer(sticky, data=request.data, partial=True)
95+
if serializer.is_valid():
96+
serializer.save()
97+
return Response(serializer.data, status=status.HTTP_200_OK)
98+
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
99+
100+
@sticky_docs(
101+
operation_id="delete_sticky",
102+
summary="Delete a sticky",
103+
description="Delete a sticky by its ID",
104+
responses={204: DELETED_RESPONSE},
105+
)
106+
def destroy(self, request, slug, pk):
107+
sticky = self.get_object()
108+
sticky.delete()
109+
return Response(status=status.HTTP_204_NO_CONTENT)

apps/api/plane/utils/openapi/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@
140140
WORKSPACE_MEMBER_EXAMPLE,
141141
PROJECT_MEMBER_EXAMPLE,
142142
CYCLE_ISSUE_EXAMPLE,
143+
STICKY_EXAMPLE,
143144
)
144145

145146
# Helper decorators
@@ -292,6 +293,7 @@
292293
"WORKSPACE_MEMBER_EXAMPLE",
293294
"PROJECT_MEMBER_EXAMPLE",
294295
"CYCLE_ISSUE_EXAMPLE",
296+
"STICKY_EXAMPLE",
295297
# Decorators
296298
"workspace_docs",
297299
"project_docs",

apps/api/plane/utils/openapi/decorators.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,3 +262,18 @@ def state_docs(**kwargs):
262262
}
263263

264264
return extend_schema(**_merge_schema_options(defaults, kwargs))
265+
266+
def sticky_docs(**kwargs):
267+
"""Decorator for sticky management endpoints"""
268+
defaults = {
269+
"tags": ["Stickies"],
270+
"summary": "Endpoints for sticky create/update/delete and fetch sticky details",
271+
"parameters": [WORKSPACE_SLUG_PARAMETER],
272+
"responses": {
273+
401: UNAUTHORIZED_RESPONSE,
274+
403: FORBIDDEN_RESPONSE,
275+
404: NOT_FOUND_RESPONSE,
276+
},
277+
}
278+
279+
return extend_schema(**_merge_schema_options(defaults, kwargs))

apps/api/plane/utils/openapi/examples.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -672,6 +672,15 @@
672672
},
673673
)
674674

675+
STICKY_EXAMPLE = OpenApiExample(
676+
name="Sticky",
677+
value={
678+
"id": "550e8400-e29b-41d4-a716-446655440000",
679+
"name": "Sticky 1",
680+
"description_html": "<p>Sticky 1 description</p>",
681+
"created_at": "2024-01-01T10:30:00Z",
682+
},
683+
)
675684

676685
# Sample data for different entity types
677686
SAMPLE_ISSUE = {
@@ -781,6 +790,13 @@
781790
"created_at": "2024-01-01T10:30:00Z",
782791
}
783792

793+
SAMPLE_STICKY = {
794+
"id": "550e8400-e29b-41d4-a716-446655440000",
795+
"name": "Sticky 1",
796+
"description_html": "<p>Sticky 1 description</p>",
797+
"created_at": "2024-01-01T10:30:00Z",
798+
}
799+
784800
# Mapping of schema types to sample data
785801
SCHEMA_EXAMPLES = {
786802
"Issue": SAMPLE_ISSUE,
@@ -795,6 +811,7 @@
795811
"Activity": SAMPLE_ACTIVITY,
796812
"Intake": SAMPLE_INTAKE,
797813
"CycleIssue": SAMPLE_CYCLE_ISSUE,
814+
"Sticky": SAMPLE_STICKY,
798815
}
799816

800817

0 commit comments

Comments
 (0)