Skip to content

Conversation

@Saurabhkmr98
Copy link
Member

@Saurabhkmr98 Saurabhkmr98 commented Nov 19, 2025

Description

  • Added external apis for CRUD Sticky

Type of Change

  • Bug fix (non-breaking change which fixes an issue)
  • Feature (non-breaking change which adds functionality)
  • Improvement (change that would cause existing functionality to not work as expected)
  • Code refactoring
  • Performance improvements
  • Documentation update

Summary by CodeRabbit

  • New Features
    • Added Stickies API: create, list (paginated, default 20), retrieve, update, and delete workspace-scoped sticky notes for the current user, with server-side validation and permission checks.
  • Documentation
    • Added OpenAPI docs, examples, and a docs helper for Stickies to improve request/response schemas and API documentation.

@makeplane
Copy link

makeplane bot commented Nov 19, 2025

Linked to Plane Work Item(s)

This comment was auto-generated by Plane

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 19, 2025

Walkthrough

Adds a Sticky feature: a DRF serializer with HTML/binary validation, a Workspace-scoped viewset with full CRUD, URL registration under workspaces//stickies/, and OpenAPI examples plus a sticky_docs decorator.

Changes

Cohort / File(s) Summary
Serializers
apps/api/plane/api/serializers/sticky.py, apps/api/plane/api/serializers/__init__.py
New StickySerializer (BaseSerializer) exposing all fields; workspace and owner read-only, name optional; sanitizes/validates description_html and validates description_binary; StickySerializer exported from package __init__.py.
Views
apps/api/plane/api/views/sticky.py, apps/api/plane/api/views/__init__.py
New StickyViewSet (CRUD: create, list, retrieve, partial_update, destroy) scoped by workspace slug and owner, uses WorkspaceUserPermission, serializer_class=StickySerializer, use_read_replica=True; re-exported in views __init__.py.
URL Routing
apps/api/plane/api/urls/sticky.py, apps/api/plane/api/urls/__init__.py
New DRF DefaultRouter registering StickyViewSet as "stickies" (basename "workspace-stickies") and included under workspaces/<str:slug>/; main urlpatterns extended to include sticky patterns.
OpenAPI / Docs
apps/api/plane/utils/openapi/examples.py, apps/api/plane/utils/openapi/decorators.py, apps/api/plane/utils/openapi/__init__.py
Added STICKY_EXAMPLE and SAMPLE_STICKY, updated SCHEMA_EXAMPLES; new sticky_docs(**kwargs) decorator with default tags/summary/WORKSPACE_SLUG_PARAMETER and standard error responses; exported STICKY_EXAMPLE in __init__.py.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Router
    participant StickyViewSet
    participant StickySerializer
    participant StickyModel

    Client->>Router: POST /workspaces/{slug}/stickies/
    Router->>StickyViewSet: create(request, slug)
    StickyViewSet->>StickySerializer: validate(data)
    StickySerializer->>StickySerializer: validate_html_content / validate_binary_data
    alt validation success
        StickySerializer-->>StickyViewSet: sanitized data
        StickyViewSet->>StickyModel: create(instance with workspace_id, owner_id)
        StickyModel-->>StickyViewSet: instance
        StickyViewSet->>StickySerializer: serialize(instance)
        StickySerializer-->>Client: 201 + JSON
    else validation error
        StickySerializer-->>Client: 400 + errors
    end

    Client->>Router: GET /workspaces/{slug}/stickies/?description=...
    Router->>StickyViewSet: list(request, slug)
    StickyViewSet->>StickyModel: queryset filtered by workspace slug & owner (select_related)
    StickyModel-->>StickyViewSet: list
    StickyViewSet->>StickySerializer: serialize(page)
    StickySerializer-->>Client: 200 + paginated JSON
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

  • Review points:
    • Serializer: HTML sanitization flow and ValidationError messages in serializers/sticky.py.
    • Viewset: queryset scoping by workspace slug/owner, pagination behavior, and permission enforcement in views/sticky.py.
    • URL registration basename and inclusion path in urls/sticky.py.
    • OpenAPI decorator defaults and example wiring in utils/openapi/*.

Poem

🐇
I hopped a sticky into code today,
Cleaned its HTML so it could safely stay.
I left a binary nibble neat and small,
Under workspaces we hop, we list, we call.
A tiny note — typed, saved, and ready to play.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes the main addition of external APIs for sticky CRUD operations, directly matching the primary change in the changeset.
Description check ✅ Passed The description is minimal but covers the core objective. It identifies the change type as a feature and briefly states the purpose, though it lacks detailed context about what sticky APIs do or how they work.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat-new_external_apis

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2529059 and 643c15f.

📒 Files selected for processing (1)
  • apps/api/plane/api/views/sticky.py (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/api/plane/api/views/sticky.py
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Analyze (javascript)

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (3)
apps/api/plane/api/serializers/sticky.py (1)

1-30: StickySerializer validation is functionally sound; consider a few refinements

The overall structure looks good: you’re sanitizing HTML, validating binary data, and preserving read-only workspace/owner. A few small improvements to consider:

  1. Chain super().validate to preserve base behavior

Even though ModelSerializer.validate currently just returns attrs, it’s safer to keep the chain intact (and future‑proof against changes in BaseSerializer):

-    def validate(self, data):
+    def validate(self, data):
+        data = super().validate(data)
  1. Use field-specific error keys for HTML to match DRF error shapes

Right now HTML errors are returned under a generic "error" key, while binary errors are properly scoped to "description_binary". For consistency with other serializers and easier client‑side mapping, consider:

-            if not is_valid:
-                raise serializers.ValidationError({"error": "html content is not valid"})
+            if not is_valid:
+                raise serializers.ValidationError(
+                    {"description_html": "Invalid HTML content"}
+                )
  1. (Optional) Surface or log the validator’s error_msg

You’re currently discarding error_msg from validate_html_content / validate_binary_data. If you don’t want to expose detailed reasons to clients, it may still be useful to log them for debugging, or at least attach them to the message when safe.

Overall, no blocking issues; these are consistency/robustness tweaks.

apps/api/plane/utils/openapi/decorators.py (1)

265-279: sticky_docs decorator matches existing OpenAPI decorator patterns

The defaults (tags, workspace slug parameter, and 401/403/404 responses) are consistent with the other *_docs helpers and work correctly with _merge_schema_options, so per-endpoint overrides will behave as expected. The extra default summary is fine, though it will usually be overridden by call-site summaries.

apps/api/plane/api/views/sticky.py (1)

45-51: Prefer get_serializer(...) over direct StickySerializer(...) for consistency and context

In create, list (the pagination lambda), and partial_update, you instantiate StickySerializer directly. This works, but it bypasses BaseViewSet/DRF’s get_serializer, which typically injects useful context like request, view, and format, and centralizes serializer selection.

You can simplify and future‑proof these methods by leaning on get_serializer:

@@ def create(self, request, slug):
-        serializer = StickySerializer(data=request.data)
+        serializer = self.get_serializer(data=request.data)
@@ def list(self, request, slug):
-        return self.paginate(
-            request=request,
-            queryset=(stickies),
-            on_results=lambda stickies: StickySerializer(stickies, many=True).data,
-            default_per_page=20,
-        )
+        return self.paginate(
+            request=request,
+            queryset=stickies,
+            on_results=lambda stickies: self.get_serializer(stickies, many=True).data,
+            default_per_page=20,
+        )
@@ def partial_update(self, request, slug, pk):
-        serializer = StickySerializer(sticky, data=request.data, partial=True)
+        serializer = self.get_serializer(sticky, data=request.data, partial=True)

This keeps behavior aligned with other viewsets and makes later serializer changes (e.g., swapping StickySerializer or using mixins) less error‑prone.

Also applies to: 63-74, 93-99

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d650323 and 6b947c9.

📒 Files selected for processing (9)
  • apps/api/plane/api/serializers/__init__.py (1 hunks)
  • apps/api/plane/api/serializers/sticky.py (1 hunks)
  • apps/api/plane/api/urls/__init__.py (2 hunks)
  • apps/api/plane/api/urls/sticky.py (1 hunks)
  • apps/api/plane/api/views/__init__.py (1 hunks)
  • apps/api/plane/api/views/sticky.py (1 hunks)
  • apps/api/plane/utils/openapi/__init__.py (2 hunks)
  • apps/api/plane/utils/openapi/decorators.py (1 hunks)
  • apps/api/plane/utils/openapi/examples.py (3 hunks)
🧰 Additional context used
🧬 Code graph analysis (5)
apps/api/plane/api/serializers/sticky.py (2)
apps/api/plane/api/serializers/base.py (1)
  • BaseSerializer (5-114)
apps/api/plane/utils/content_validator.py (2)
  • validate_html_content (212-244)
  • validate_binary_data (25-66)
apps/api/plane/api/urls/sticky.py (1)
apps/api/plane/api/views/sticky.py (1)
  • StickyViewSet (20-110)
apps/api/plane/api/views/__init__.py (1)
apps/api/plane/api/views/sticky.py (1)
  • StickyViewSet (20-110)
apps/api/plane/api/serializers/__init__.py (2)
apps/api/plane/api/serializers/member.py (1)
  • ProjectMemberSerializer (11-39)
apps/api/plane/api/serializers/sticky.py (1)
  • StickySerializer (8-30)
apps/api/plane/api/views/sticky.py (5)
apps/api/plane/utils/permissions/workspace.py (1)
  • WorkspaceUserPermission (99-106)
apps/api/plane/db/models/workspace.py (1)
  • Workspace (115-178)
apps/api/plane/api/serializers/sticky.py (1)
  • StickySerializer (8-30)
apps/api/plane/utils/openapi/decorators.py (1)
  • sticky_docs (266-279)
apps/api/plane/utils/openapi/responses.py (1)
  • create_paginated_response (354-401)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Analyze (javascript)
🔇 Additional comments (7)
apps/api/plane/api/views/__init__.py (1)

57-59: Exposing WorkspaceInvitationsViewset and StickyViewSet via package init looks consistent

The new imports match the existing pattern of surfacing viewsets from this package; no issues from a wiring perspective. Please just ensure router/url modules are importing from here as intended after this change.

apps/api/plane/api/serializers/__init__.py (1)

57-58: Serializer exports for ProjectMemberSerializer and StickySerializer are aligned with existing pattern

Re-exporting these serializers from the package init is consistent with how other serializers are surfaced and will support dynamic expansion/imports in BaseSerializer.

apps/api/plane/api/urls/__init__.py (1)

12-26: Sticky URL patterns are wired consistently with existing modules

Importing sticky_patterns and splatting them into urlpatterns follows the same pattern as other feature modules. This should correctly expose sticky endpoints without altering existing routes’ behavior.

apps/api/plane/utils/openapi/__init__.py (1)

93-144: OpenAPI example wiring for Sticky is consistent with existing examples

Adding STICKY_EXAMPLE to the examples import list and __all__ aligns with how other example objects are exposed from this module. That should make it straightforward to reference in sticky_docs and responses.

Also applies to: 172-297

apps/api/plane/api/urls/sticky.py (1)

1-12: Workspace-scoped Sticky routing looks correct

Registering StickyViewSet on the DefaultRouter and mounting it under workspaces/<str:slug>/ cleanly yields routes like workspaces/<slug>/stickies/ and workspaces/<slug>/stickies/<pk>/, matching the viewset’s use of slug in get_queryset and method signatures.

apps/api/plane/utils/openapi/examples.py (1)

675-683: Sticky OpenAPI examples and schema mapping are consistent

STICKY_EXAMPLE, SAMPLE_STICKY, and the "Sticky": SAMPLE_STICKY entry in SCHEMA_EXAMPLES line up with each other and with the use of create_paginated_response(..., "Sticky", ...), so Sticky endpoints will have coherent single-item and paginated examples.

Also applies to: 793-799, 814-815

apps/api/plane/api/views/sticky.py (1)

20-35: Queryset scoping for StickyViewSet is appropriate

Filtering by workspace__slug from kwargs["slug"] and owner_id=self.request.user.id, plus select_related("workspace", "owner"), gives you correctly scoped, efficient queries and pairs well with WorkspaceUserPermission to avoid leaking stickies across workspaces or users.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
apps/api/plane/api/views/sticky.py (1)

64-64: Consider using None as the default for the query parameter.

Using False as the default for the query parameter works but is unconventional. An empty string "" or None would be more idiomatic for optional string parameters.

-    query = request.query_params.get("query", False)
+    query = request.query_params.get("query")
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6b947c9 and 2529059.

📒 Files selected for processing (1)
  • apps/api/plane/api/views/sticky.py (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
apps/api/plane/api/views/sticky.py (6)
apps/api/plane/utils/permissions/workspace.py (1)
  • WorkspaceUserPermission (99-106)
apps/api/plane/db/models/workspace.py (1)
  • Workspace (115-178)
apps/api/plane/api/serializers/sticky.py (1)
  • StickySerializer (8-30)
apps/api/plane/utils/openapi/decorators.py (1)
  • sticky_docs (266-279)
apps/api/plane/utils/openapi/responses.py (1)
  • create_paginated_response (354-401)
apps/api/plane/app/views/workspace/sticky.py (1)
  • WorkspaceStickyViewSet (12-60)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Analyze (javascript)
🔇 Additional comments (7)
apps/api/plane/api/views/sticky.py (7)

1-17: LGTM: Imports are well-organized.

All necessary imports are present and properly structured for the viewset functionality and OpenAPI documentation.


20-24: LGTM: ViewSet configuration is appropriate.

The class attributes are correctly configured with proper permission checking and read replica optimization.


26-34: LGTM: Query optimization and filtering are correct.

The queryset properly filters by workspace and owner while using select_related for query optimization. The owner filtering ensures users can only access their own stickies.


82-84: LGTM: Retrieve method is correctly implemented.

The method properly uses self.get_object() which handles 404 responses and applies the queryset filtering for security.


93-99: LGTM: Partial update is correctly implemented.

The method properly validates and applies partial updates, with security ensured through the queryset filtering by owner.


107-110: LGTM: Delete method is correctly implemented.

The method properly deletes the sticky with security ensured through queryset filtering, and returns the appropriate 204 status code.


63-74: Verify the ordering field choice and document the design intent.

The Sticky model has a sort_order field and default ordering by "-created_at", yet WorkspaceStickyViewSet orders by "-sort_order" while the external API endpoint (line 65) orders by "-created_at". This inconsistency is confirmed and could be intentional (different UX for internal vs. external APIs) or an oversight. Please clarify the design intent and add a code comment explaining the choice if this difference is deliberate.

Comment on lines +45 to +51
def create(self, request, slug):
workspace = Workspace.objects.get(slug=slug)
serializer = StickySerializer(data=request.data)
if serializer.is_valid():
serializer.save(workspace_id=workspace.id, owner_id=request.user.id)
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Handle missing workspace gracefully.

Line 46 uses Workspace.objects.get(slug=slug) which will raise an unhandled Workspace.DoesNotExist exception if the workspace is not found, resulting in a 500 error instead of a proper 404 response.

Apply this diff to handle the missing workspace gracefully:

+from django.shortcuts import get_object_or_404
+
 def create(self, request, slug):
-    workspace = Workspace.objects.get(slug=slug)
+    workspace = get_object_or_404(Workspace, slug=slug)
     serializer = StickySerializer(data=request.data)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def create(self, request, slug):
workspace = Workspace.objects.get(slug=slug)
serializer = StickySerializer(data=request.data)
if serializer.is_valid():
serializer.save(workspace_id=workspace.id, owner_id=request.user.id)
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def create(self, request, slug):
workspace = get_object_or_404(Workspace, slug=slug)
serializer = StickySerializer(data=request.data)
if serializer.is_valid():
serializer.save(workspace_id=workspace.id, owner_id=request.user.id)
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
🤖 Prompt for AI Agents
In apps/api/plane/api/views/sticky.py around lines 45 to 51, the call
Workspace.objects.get(slug=slug) can raise Workspace.DoesNotExist and cause a
500; change this to handle missing workspace by either using
django.shortcuts.get_object_or_404(Workspace, slug=slug) or wrapping the get in
a try/except Workspace.DoesNotExist and returning Response({"detail": "Workspace
not found."}, status=status.HTTP_404_NOT_FOUND) before proceeding; ensure
imports are added (get_object_or_404) or the Workspace.DoesNotExist is caught
and the function exits after returning the 404 response.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants