Skip to content

[SILO-1028] feat: Project Summary external API#8661

Open
Saurabhkmr98 wants to merge 2 commits intopreviewfrom
feat-project_metadata_api
Open

[SILO-1028] feat: Project Summary external API#8661
Saurabhkmr98 wants to merge 2 commits intopreviewfrom
feat-project_metadata_api

Conversation

@Saurabhkmr98
Copy link
Member

@Saurabhkmr98 Saurabhkmr98 commented Feb 25, 2026

Description

  • Added new external API that gives a count summary for all sub entities
  • Includes a fields filter to get only required field counts

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

Screenshots and Media (if applicable)

Sample response

{
    "id": "d61d47b1-93ea-41b9-8e51-0cfb3d34e327",
    "name": "git state skip",
    "identifier": "GITST",
    "counts": {
        "intakes": 2,
        "states": 5,
        "modules": 3,
        "labels": 2,
        "cycles": 2,
        "members": 4,
        "issues": 8,
        "pages": 2
    }
}

API Query Profile

Screenshot 2026-02-26 at 1 46 38 PM

Test Scenarios

References

Summary by CodeRabbit

  • New Features

    • Project summary endpoint with selectable counts (members, states, labels, cycles, modules, issues, intakes, pages).
    • Full estimates API: create/read/update/delete project estimates and manage estimate points (bulk create, update, delete).
  • Documentation

    • OpenAPI docs and examples added for estimates and estimate points (request/response examples and path parameters).
  • Database

    • Migration changing estimate type to explicit choices (categories/points).

@makeplane
Copy link

makeplane bot commented Feb 25, 2026

Linked to Plane Work Item(s)

This comment was auto-generated by Plane

@Saurabhkmr98 Saurabhkmr98 changed the title [SILO-1029] feat: Project Summary external API [SILO-1028] feat: Project Summary external API Feb 25, 2026
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 25, 2026

📝 Walkthrough

Walkthrough

Adds project summary and full estimate management (estimates and estimate points) endpoints, updates serializers and model choice for Estimate.type, adds migration, and expands OpenAPI parameters/examples and URL routing; permissions and validation are applied across new endpoints.

Changes

Cohort / File(s) Summary
Project Routing & Exports
apps/api/plane/api/urls/project.py, apps/api/plane/api/views/__init__.py
Registered ProjectSummaryAPIEndpoint route and re-exported it from the views package.
Project Views
apps/api/plane/api/views/project.py
Added ALLOWED_PROJECT_SUMMARY_FIELDS and ProjectSummaryAPIEndpoint (WorkSpaceAdminPermission) with _get_all_summary_counts using ORM subqueries + Coalesce; small GET result wrapping refactor.
Estimate Views
apps/api/plane/api/views/estimate.py
New endpoints: ProjectEstimateAPIEndpoint, EstimatePointListCreateAPIEndpoint, EstimatePointDetailAPIEndpoint providing CRUD for estimates and their points with permission checks, validation, and OpenAPI docs.
Estimate URLs
apps/api/plane/api/urls/estimate.py
New URL patterns for project-level estimate and estimate-point list/detail routes.
Serializers
apps/api/plane/api/serializers/estimate.py, apps/api/plane/api/serializers/__init__.py
Added EstimateSerializer (create uses context to set workspace/project); expanded EstimatePointSerializer with validation and full-field representation; updated package exports.
Model & Migration
apps/api/plane/db/models/estimate.py, apps/api/plane/db/migrations/0121_alter_estimate_type.py
Introduced EstimateType TextChoices and updated Estimate.type to use choices/default; added migration altering the field to use new choices & default.
OpenAPI: params/examples/decorators
apps/api/plane/utils/openapi/__init__.py, .../decorators.py, .../examples.py, .../parameters.py
Added ESTIMATE_ID_PARAMETER, new examples and sample data for Estimate/EstimatePoint, and helper decorators estimate_docs / estimate_point_docs; updated exports and schema examples.

Sequence Diagram(s)

sequenceDiagram
  participant Client as Client
  participant API as ProjectSummaryAPIEndpoint
  participant Auth as Permissions
  participant DB as Database

  Client->>API: GET /workspaces/{slug}/projects/{id}/summary?fields=members,issues,...
  API->>Auth: check WorkSpaceAdminPermission
  Auth-->>API: allowed / denied
  API->>DB: aggregated ORM query (per-field subqueries + Coalesce)
  DB-->>API: annotated counts
  API-->>Client: 200 OK with JSON summary
Loading
sequenceDiagram
  participant Client as Client
  participant API as EstimatesAPI
  participant Auth as Permissions
  participant DB as Database
  participant Serializer as Serializer/Validation

  Client->>API: POST/GET/PATCH/DELETE /workspaces/{slug}/projects/{id}/estimate...
  API->>Auth: check ProjectEntityPermission
  Auth-->>API: allowed / denied
  API->>Serializer: validate request data (Estimate / EstimatePoint)
  Serializer-->>API: valid / errors
  API->>DB: create/read/update/delete estimate or estimate points
  DB-->>API: persisted/read objects
  API-->>Client: 2XX or 4XX/409 responses (with OpenAPI docs)
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Poem

🐰 I counted fields with a twitch and a hop,
Members, labels, and points that won't stop.
New endpoints and docs in a tidy array,
I nibbled a carrot and scurried away. 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 72.73% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title '[SILO-1028] feat: Project Summary external API' clearly and concisely describes the main change—adding a new external API for project summaries—which aligns with the primary objective in the PR.
Description check ✅ Passed The PR description includes a clear summary of changes, specifies the feature type, and provides sample response data and performance profiling. However, the 'Test Scenarios' section is empty, which is a required section in the template.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat-project_metadata_api

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: 1

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

577-595: Two DB round-trips, not one as the comment claims.

Line 582 executes a Project.objects.filter(...).first() to check existence and fetch project.id / project.name. Line 666 inside _get_all_summary_counts runs a second query for the annotations. You can fold the name/id retrieval into the annotated query by adding "id" and "name" to values_list, removing the first lookup entirely.

♻️ Sketch — single-query approach
     def get(self, request, slug, project_id):
-        project = Project.objects.filter(pk=project_id, workspace__slug=slug).first()
-        if not project:
-            return Response({"error": "Project not found"}, status=status.HTTP_404_NOT_FOUND)
         fields = request.GET.get("fields", "").split(",")
         requested_fields = set(filter(None, (f.strip() for f in fields))) & set(ALLOWED_PROJECT_SUMMARY_FIELDS)
         if not requested_fields:
             requested_fields = set(ALLOWED_PROJECT_SUMMARY_FIELDS)
 
-        counts = self._get_all_summary_counts(project_id, requested_fields)
-        summary = {field: counts[field] for field in requested_fields}
-        summary["project_id"] = project.id
-        summary["project_name"] = project.name
+        result = self._get_all_summary_counts(project_id, slug, requested_fields)
+        if result is None:
+            return Response({"error": "Project not found"}, status=status.HTTP_404_NOT_FOUND)
+        summary = {field: result[field] for field in requested_fields}
+        summary["project_id"] = result["id"]
+        summary["project_name"] = result["name"]
         return Response(summary, status=status.HTTP_200_OK)

Then in _get_all_summary_counts, filter by both pk and workspace__slug, and include "id" and "name" in values_list.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/api/plane/api/views/project.py` around lines 577 - 595, The current
get() does an extra Project.objects.filter(...).first() DB hit; remove that
initial lookup and rely on _get_all_summary_counts to both compute counts and
return the project's id and name in the same query: change
_get_all_summary_counts to filter by pk and workspace__slug (use both slug and
project_id), include "id" and "name" in its values_list/annotations and return
them along with the counts, then in get() call that method and extract
project_id/project_name from its result instead of doing
Project.objects.filter(...).first(); this collapses the two DB round-trips into
a single query.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/api/plane/api/views/project.py`:
- Around line 573-575: ProjectSummaryAPIEndpoint currently uses
WorkSpaceAdminPermission (allowing Admin and Member) while other project
endpoints use ProjectBasePermission (which permits any workspace member on GET);
update ProjectSummaryAPIEndpoint.permission_classes to ProjectBasePermission to
match the pattern unless the intent is to restrict summary counts to admins
only—if admin-only behavior is intended, add an explicit comment explaining that
and keep WorkSpaceAdminPermission; reference the class name
ProjectSummaryAPIEndpoint and the permission symbols WorkSpaceAdminPermission
and ProjectBasePermission when making the change.

---

Nitpick comments:
In `@apps/api/plane/api/views/project.py`:
- Around line 577-595: The current get() does an extra
Project.objects.filter(...).first() DB hit; remove that initial lookup and rely
on _get_all_summary_counts to both compute counts and return the project's id
and name in the same query: change _get_all_summary_counts to filter by pk and
workspace__slug (use both slug and project_id), include "id" and "name" in its
values_list/annotations and return them along with the counts, then in get()
call that method and extract project_id/project_name from its result instead of
doing Project.objects.filter(...).first(); this collapses the two DB round-trips
into a single query.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9425c66 and 86a787b.

📒 Files selected for processing (3)
  • apps/api/plane/api/urls/project.py
  • apps/api/plane/api/views/__init__.py
  • apps/api/plane/api/views/project.py

Comment on lines +573 to +575
class ProjectSummaryAPIEndpoint(BaseAPIView):
permission_classes = [WorkSpaceAdminPermission]
use_read_replica = True
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 | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check WorkSpaceAdminPermission implementation
echo "=== WorkSpaceAdminPermission ==="
rg -n "class WorkSpaceAdminPermission" --type py -A 15

echo ""
echo "=== ProjectBasePermission ==="
rg -n "class ProjectBasePermission" --type py -A 15

echo ""
echo "=== ProjectSummaryAPIEndpoint context ==="
rg -n "class ProjectSummaryAPIEndpoint" --type py -B 2 -A 20

Repository: makeplane/plane

Length of output: 7442


🏁 Script executed:

#!/bin/bash
# Check what role types exist
echo "=== Role definitions (Admin, Member constants) ==="
rg -n "^(Admin|Member)\s*=" --type py | head -20

echo ""
echo "=== Other ProjectAPIView endpoints and their permissions ==="
rg -n "class Project.*APIView.*|permission_classes" apps/api/plane/api/views/project.py | head -40

echo ""
echo "=== Check if ProjectBasePermission is used elsewhere ==="
rg -l "ProjectBasePermission" --type py

Repository: makeplane/plane

Length of output: 1141


🏁 Script executed:

#!/bin/bash
# Get full ProjectBasePermission implementation
echo "=== Full ProjectBasePermission implementation ==="
rg -n "class ProjectBasePermission" --type py -A 40 | head -60

echo ""
echo "=== Check if there are project-level roles/permissions ==="
rg -n "ProjectMember|project.*role" --type py -i | grep -E "(class|def)" | head -20

echo ""
echo "=== Check context around ProjectSummaryAPIEndpoint for comments/rationale ==="
rg -n "class ProjectSummaryAPIEndpoint" --type py -B 10 -A 30

Repository: makeplane/plane

Length of output: 10560


Reconsider permission consistency — WorkSpaceAdminPermission allows both admin and member roles, but other project endpoints use ProjectBasePermission

WorkSpaceAdminPermission allows workspace members with role Admin or Member to access this endpoint. However, all other project API endpoints (ProjectListCreateAPIEndpoint, ProjectDetailAPIEndpoint, ProjectArchiveUnarchiveAPIEndpoint) use ProjectBasePermission, which for GET requests permits any workspace member. This endpoint is inconsistent with the pattern. If project summary counts are sensitive and should be restricted to admins/members only, confirm this is intentional; otherwise, consider using ProjectBasePermission for consistency.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/api/plane/api/views/project.py` around lines 573 - 575,
ProjectSummaryAPIEndpoint currently uses WorkSpaceAdminPermission (allowing
Admin and Member) while other project endpoints use ProjectBasePermission (which
permits any workspace member on GET); update
ProjectSummaryAPIEndpoint.permission_classes to ProjectBasePermission to match
the pattern unless the intent is to restrict summary counts to admins only—if
admin-only behavior is intended, add an explicit comment explaining that and
keep WorkSpaceAdminPermission; reference the class name
ProjectSummaryAPIEndpoint and the permission symbols WorkSpaceAdminPermission
and ProjectBasePermission when making the change.

dheeru0198
dheeru0198 previously approved these changes Feb 26, 2026
@sriramveeraghanta
Copy link
Member

Ideally if we are creating a structure like this, it will be scalable.

{
  id: "d61d47b1-93ea-41b9-8e51-0cfb3d34e327"
  name: "Project Name"
  identifier: "WEB",
  counts: {
    "intakes": 2,
    "modules": 3,
    "cycles": 2,
    "issues": 8,
    "labels": 2,
    "members": 4,
    "pages": 2,
    "states": 5,
  }
}

@Saurabhkmr98 Saurabhkmr98 marked this pull request as draft February 26, 2026 12:02
@Saurabhkmr98 Saurabhkmr98 marked this pull request as ready for review February 26, 2026 12:16
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 (2)
apps/api/plane/api/views/project.py (2)

591-591: Redundant dict comprehension.

_get_all_summary_counts already returns a dict containing exactly the requested_fields, so this comprehension just rebuilds the same dict.

♻️ Suggested simplification
-        counts_dict = {field: counts[field] for field in requested_fields}
+        counts_dict = counts
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/api/plane/api/views/project.py` at line 591, The dict comprehension
rebuilding counts_dict is redundant because _get_all_summary_counts already
returns a dict keyed by requested_fields; replace the reconstruction by using
the returned dict directly (e.g., use the counts variable returned from
_get_all_summary_counts where counts_dict is currently used) and remove the
comprehension that reassigns counts_dict = {field: counts[field] for field in
requested_fields}; ensure any references to counts_dict now point to counts (or
rename counts to counts_dict if you prefer that identifier).

576-598: Missing OpenAPI documentation for this endpoint.

All other endpoints in this file use @project_docs decorator for API documentation. Consider adding documentation for consistency with the rest of the file and to ensure the external API is properly documented.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/api/plane/api/views/project.py` around lines 576 - 598, Add the missing
OpenAPI documentation by decorating the get method with the same `@project_docs`
decorator pattern used elsewhere in this file; update the get method (function
name: get) to include a `@project_docs` call that documents the endpoint
summary/description, query parameter "fields" (allowed values from
ALLOWED_PROJECT_SUMMARY_FIELDS), response schema (including id, name,
identifier, counts) and 404/200 responses so the endpoint is included in the
generated API docs and consistent with other endpoints in this module.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/api/plane/api/views/project.py`:
- Around line 609-614: The new "members" subquery on ProjectMember (the lambda
returning ProjectMember.objects.filter(project_id=OuterRef("pk"),
is_active=True).values("project_id").annotate(count=Count("*")).values("count"))
is missing the same bot-exclusion used elsewhere; update that filter to include
member__is_bot=False so it matches the existing total_members logic (i.e., add
member__is_bot=False to the filter on ProjectMember in the members lambda) to
ensure consistent member counts across endpoints.

---

Nitpick comments:
In `@apps/api/plane/api/views/project.py`:
- Line 591: The dict comprehension rebuilding counts_dict is redundant because
_get_all_summary_counts already returns a dict keyed by requested_fields;
replace the reconstruction by using the returned dict directly (e.g., use the
counts variable returned from _get_all_summary_counts where counts_dict is
currently used) and remove the comprehension that reassigns counts_dict =
{field: counts[field] for field in requested_fields}; ensure any references to
counts_dict now point to counts (or rename counts to counts_dict if you prefer
that identifier).
- Around line 576-598: Add the missing OpenAPI documentation by decorating the
get method with the same `@project_docs` decorator pattern used elsewhere in this
file; update the get method (function name: get) to include a `@project_docs` call
that documents the endpoint summary/description, query parameter "fields"
(allowed values from ALLOWED_PROJECT_SUMMARY_FIELDS), response schema (including
id, name, identifier, counts) and 404/200 responses so the endpoint is included
in the generated API docs and consistent with other endpoints in this module.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 86a787b and dda0739.

📒 Files selected for processing (1)
  • apps/api/plane/api/views/project.py

Comment on lines +609 to +614
"members": lambda: (
ProjectMember.objects.filter(project_id=OuterRef("pk"), is_active=True)
.values("project_id")
.annotate(count=Count("*"))
.values("count")
),
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 | 🟡 Minor

Missing member__is_bot=False filter — inconsistent with existing member count logic.

The existing total_members annotation at lines 105-106 and 320-321 filters out bot members with member__is_bot=False, but this new members subquery does not include that filter. This inconsistency will cause different member counts between the project list/detail endpoints and this summary endpoint.

🐛 Proposed fix
             "members": lambda: (
-                ProjectMember.objects.filter(project_id=OuterRef("pk"), is_active=True)
+                ProjectMember.objects.filter(project_id=OuterRef("pk"), is_active=True, member__is_bot=False)
                 .values("project_id")
                 .annotate(count=Count("*"))
                 .values("count")
             ),
📝 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
"members": lambda: (
ProjectMember.objects.filter(project_id=OuterRef("pk"), is_active=True)
.values("project_id")
.annotate(count=Count("*"))
.values("count")
),
"members": lambda: (
ProjectMember.objects.filter(project_id=OuterRef("pk"), is_active=True, member__is_bot=False)
.values("project_id")
.annotate(count=Count("*"))
.values("count")
),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/api/plane/api/views/project.py` around lines 609 - 614, The new
"members" subquery on ProjectMember (the lambda returning
ProjectMember.objects.filter(project_id=OuterRef("pk"),
is_active=True).values("project_id").annotate(count=Count("*")).values("count"))
is missing the same bot-exclusion used elsewhere; update that filter to include
member__is_bot=False so it matches the existing total_members logic (i.e., add
member__is_bot=False to the filter on ProjectMember in the members lambda) to
ensure consistent member counts across endpoints.

@Saurabhkmr98 Saurabhkmr98 force-pushed the feat-project_metadata_api branch from 52a255f to dda0739 Compare February 26, 2026 14:42
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: 2

🧹 Nitpick comments (3)
apps/api/plane/utils/openapi/examples.py (2)

690-698: Minor inconsistency: ESTIMATE_EXAMPLE missing fields present in SAMPLE_ESTIMATE.

The ESTIMATE_EXAMPLE response example lacks type, last_used, and created_at fields that are present in SAMPLE_ESTIMATE (lines 867-874). Consider aligning these for consistency in the API documentation.

🔧 Suggested enhancement
 ESTIMATE_EXAMPLE = OpenApiExample(
     name="Estimate",
     value={
         "id": "550e8400-e29b-41d4-a716-446655440000",
         "name": "Estimate 1",
         "description": "Estimate 1 description",
+        "type": "categories",
+        "last_used": False,
+        "created_at": "2024-01-01T10:30:00Z",
     },
     description="Example response for an estimate",
 )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/api/plane/utils/openapi/examples.py` around lines 690 - 698, Update the
OpenApiExample ESTIMATE_EXAMPLE so its value includes the same fields as
SAMPLE_ESTIMATE: add "type", "last_used", and "created_at" (use the same example
values/formats as SAMPLE_ESTIMATE) to keep the API documentation consistent;
edit the ESTIMATE_EXAMPLE object (the OpenApiExample instance named
ESTIMATE_EXAMPLE) to include these keys in its value dictionary.

710-725: Create and update examples are identical.

ESTIMATE_CREATE_EXAMPLE and ESTIMATE_UPDATE_EXAMPLE have identical values. This is acceptable if the API truly uses the same schema for both operations, but consider differentiating them slightly (e.g., "Updated Estimate 1" for the update example) to make the documentation clearer.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/api/plane/utils/openapi/examples.py` around lines 710 - 725, The create
and update OpenApiExample instances currently use identical example payloads;
update ESTIMATE_UPDATE_EXAMPLE (the OpenApiExample named
"EstimateUpdateSerializer") to show a distinct updated payload (e.g., change
"name" to "Estimate 1 Updated" and/or adjust "description" to "Updated Estimate
1 description") so the examples differ and clearly communicate the update
operation.
apps/api/plane/utils/openapi/parameters.py (1)

499-505: Consider adding an example for consistency with other parameters.

Other ID parameters in this file (e.g., CYCLE_ID_PARAMETER, MODULE_ID_PARAMETER, STATE_ID_PARAMETER) include OpenApiExample instances to enhance API documentation. Adding a similar example here would maintain consistency.

🔧 Suggested enhancement
 ESTIMATE_ID_PARAMETER = OpenApiParameter(
     name="estimate_id",
     description="Estimate ID",
     required=True,
     type=OpenApiTypes.UUID,
     location=OpenApiParameter.PATH,
+    examples=[
+        OpenApiExample(
+            name="Example estimate ID",
+            value="550e8400-e29b-41d4-a716-446655440000",
+            description="A typical estimate UUID",
+        )
+    ],
 )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/api/plane/utils/openapi/parameters.py` around lines 499 - 505,
ESTIMATE_ID_PARAMETER lacks an OpenApiExample like the other ID params; update
the ESTIMATE_ID_PARAMETER definition to include an
examples=[OpenApiExample(...)] entry (mirroring style used by CYCLE_ID_PARAMETER
/ MODULE_ID_PARAMETER / STATE_ID_PARAMETER) so the OpenAPI docs show a sample
UUID and descriptive name for the estimate_id path parameter.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/api/plane/api/serializers/estimate.py`:
- Around line 26-32: The validate method in the serializer (validate)
incorrectly rejects partial updates because of the "if not data" check and uses
a confusing error message and a stricter length cap than the model; remove or
relax the empty-data guard so partial PATCH requests are allowed (only raise if
no fields provided AND this serializer is used for full-create flows), change
the message "Estimate points are required" to something specific like "Estimate
value is required" when you do intend to require the field, and either align the
value length check with the model's max_length=255 or add a clear
comment/docstring explaining the intentional 20-character business rule; update
the validate method (function name: validate) accordingly to implement these
changes.

In `@apps/api/plane/api/views/estimate.py`:
- Around line 47-69: The code in post() calls Project.objects.get(...) and
Workspace.objects.get(...), which will raise exceptions instead of returning
None, so replace those calls with Project.objects.filter(id=project_id,
workspace__slug=slug).first() and Workspace.objects.filter(slug=slug).first()
(or equivalent) to match the existing pattern used for project_estimate; then
keep the existing if not project / if not workspace 404 responses intact so
missing records return 404 instead of raising. Ensure you update any references
to the retrieved objects (project, workspace) and maintain serializer
instantiation via self.serializer_class(..., context={"workspace": workspace,
"project": project}).

---

Nitpick comments:
In `@apps/api/plane/utils/openapi/examples.py`:
- Around line 690-698: Update the OpenApiExample ESTIMATE_EXAMPLE so its value
includes the same fields as SAMPLE_ESTIMATE: add "type", "last_used", and
"created_at" (use the same example values/formats as SAMPLE_ESTIMATE) to keep
the API documentation consistent; edit the ESTIMATE_EXAMPLE object (the
OpenApiExample instance named ESTIMATE_EXAMPLE) to include these keys in its
value dictionary.
- Around line 710-725: The create and update OpenApiExample instances currently
use identical example payloads; update ESTIMATE_UPDATE_EXAMPLE (the
OpenApiExample named "EstimateUpdateSerializer") to show a distinct updated
payload (e.g., change "name" to "Estimate 1 Updated" and/or adjust "description"
to "Updated Estimate 1 description") so the examples differ and clearly
communicate the update operation.

In `@apps/api/plane/utils/openapi/parameters.py`:
- Around line 499-505: ESTIMATE_ID_PARAMETER lacks an OpenApiExample like the
other ID params; update the ESTIMATE_ID_PARAMETER definition to include an
examples=[OpenApiExample(...)] entry (mirroring style used by CYCLE_ID_PARAMETER
/ MODULE_ID_PARAMETER / STATE_ID_PARAMETER) so the OpenAPI docs show a sample
UUID and descriptive name for the estimate_id path parameter.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between dda0739 and 52a255f.

📒 Files selected for processing (10)
  • apps/api/plane/api/serializers/__init__.py
  • apps/api/plane/api/serializers/estimate.py
  • apps/api/plane/api/urls/estimate.py
  • apps/api/plane/api/views/estimate.py
  • apps/api/plane/db/migrations/0121_alter_estimate_type.py
  • apps/api/plane/db/models/estimate.py
  • apps/api/plane/utils/openapi/__init__.py
  • apps/api/plane/utils/openapi/decorators.py
  • apps/api/plane/utils/openapi/examples.py
  • apps/api/plane/utils/openapi/parameters.py

Comment on lines 26 to 32
def validate(self, data):
if not data:
raise serializers.ValidationError("Estimate points are required")
value = data.get("value")
if value and len(value) > 20:
raise serializers.ValidationError("Value can't be more than 20 characters")
return data
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 | 🟡 Minor

Validation logic may be overly restrictive for partial updates.

  1. Line 27: if not data will reject valid partial update requests where only some fields are provided. Consider whether this check is necessary.

  2. Error message: "Estimate points are required" is confusing when this serializer handles a single point, not multiple points.

  3. Line 30: The 20-character limit on value is more restrictive than the model's max_length=255. This appears intentional based on the OpenAPI examples, but consider documenting this business rule.

🐛 Suggested fix for the empty data check
     def validate(self, data):
-        if not data:
-            raise serializers.ValidationError("Estimate points are required")
         value = data.get("value")
         if value and len(value) > 20:
-            raise serializers.ValidationError("Value can't be more than 20 characters")
+            raise serializers.ValidationError("Value cannot exceed 20 characters")
         return data
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/api/plane/api/serializers/estimate.py` around lines 26 - 32, The
validate method in the serializer (validate) incorrectly rejects partial updates
because of the "if not data" check and uses a confusing error message and a
stricter length cap than the model; remove or relax the empty-data guard so
partial PATCH requests are allowed (only raise if no fields provided AND this
serializer is used for full-create flows), change the message "Estimate points
are required" to something specific like "Estimate value is required" when you
do intend to require the field, and either align the value length check with the
model's max_length=255 or add a clear comment/docstring explaining the
intentional 20-character business rule; update the validate method (function
name: validate) accordingly to implement these changes.

Comment on lines 47 to 69
def post(self, request, slug, project_id):
project = Project.objects.get(id=project_id, workspace__slug=slug)
if not project:
return Response(status=status.HTTP_404_NOT_FOUND, data={"error": "Project not found"})

workspace = Workspace.objects.get(slug=slug)
if not workspace:
return Response(status=status.HTTP_404_NOT_FOUND, data={"error": "Workspace not found"})

project_estimate = self.get_queryset().first()
if project_estimate:
# return 409 if the project estimate already exists
return Response(
status=status.HTTP_409_CONFLICT,
data={"error": "An estimate already exists for this project", "id": str(project_estimate.id)},
)
# create the project estimate
serializer = self.serializer_class(data=request.data, context={"workspace": workspace, "project": project})
if not serializer.is_valid():
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
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 | 🔴 Critical

Model.objects.get() raises exception; subsequent checks are dead code.

Lines 48-54 use .get() which raises ObjectDoesNotExist when no matching record exists. The if not project: and if not workspace: checks are never reached because the exception is thrown first—likely resulting in a 500 error instead of the intended 404.

Use .filter().first() consistently with other methods in this file.

🐛 Proposed fix
     def post(self, request, slug, project_id):
-        project = Project.objects.get(id=project_id, workspace__slug=slug)
+        project = Project.objects.filter(id=project_id, workspace__slug=slug).first()
         if not project:
             return Response(status=status.HTTP_404_NOT_FOUND, data={"error": "Project not found"})

-        workspace = Workspace.objects.get(slug=slug)
+        workspace = Workspace.objects.filter(slug=slug).first()
         if not workspace:
             return Response(status=status.HTTP_404_NOT_FOUND, data={"error": "Workspace not found"})
📝 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 post(self, request, slug, project_id):
project = Project.objects.get(id=project_id, workspace__slug=slug)
if not project:
return Response(status=status.HTTP_404_NOT_FOUND, data={"error": "Project not found"})
workspace = Workspace.objects.get(slug=slug)
if not workspace:
return Response(status=status.HTTP_404_NOT_FOUND, data={"error": "Workspace not found"})
project_estimate = self.get_queryset().first()
if project_estimate:
# return 409 if the project estimate already exists
return Response(
status=status.HTTP_409_CONFLICT,
data={"error": "An estimate already exists for this project", "id": str(project_estimate.id)},
)
# create the project estimate
serializer = self.serializer_class(data=request.data, context={"workspace": workspace, "project": project})
if not serializer.is_valid():
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
def post(self, request, slug, project_id):
project = Project.objects.filter(id=project_id, workspace__slug=slug).first()
if not project:
return Response(status=status.HTTP_404_NOT_FOUND, data={"error": "Project not found"})
workspace = Workspace.objects.filter(slug=slug).first()
if not workspace:
return Response(status=status.HTTP_404_NOT_FOUND, data={"error": "Workspace not found"})
project_estimate = self.get_queryset().first()
if project_estimate:
# return 409 if the project estimate already exists
return Response(
status=status.HTTP_409_CONFLICT,
data={"error": "An estimate already exists for this project", "id": str(project_estimate.id)},
)
# create the project estimate
serializer = self.serializer_class(data=request.data, context={"workspace": workspace, "project": project})
if not serializer.is_valid():
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/api/plane/api/views/estimate.py` around lines 47 - 69, The code in
post() calls Project.objects.get(...) and Workspace.objects.get(...), which will
raise exceptions instead of returning None, so replace those calls with
Project.objects.filter(id=project_id, workspace__slug=slug).first() and
Workspace.objects.filter(slug=slug).first() (or equivalent) to match the
existing pattern used for project_estimate; then keep the existing if not
project / if not workspace 404 responses intact so missing records return 404
instead of raising. Ensure you update any references to the retrieved objects
(project, workspace) and maintain serializer instantiation via
self.serializer_class(..., context={"workspace": workspace, "project":
project}).

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