Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WEB-2600] fix: estimate point deletion #5762

Merged
merged 2 commits into from
Oct 7, 2024

Conversation

NarayanBavisetti
Copy link
Collaborator

@NarayanBavisetti NarayanBavisetti commented Oct 7, 2024

fix:

  • This pull request resolves the issue of not deleting an issue when its estimate point is deleted. It also adds logging for issue activity related to changes in the estimate point.

Issue Link: WEB-2600

Summary by CodeRabbit

Release Notes

  • New Features

    • Enhanced estimate point deletion process with reassignment capabilities.
    • Automatic inbox creation for projects during updates.
    • Improved activity tracking for issue updates, differentiating between removal and update actions.
  • Bug Fixes

    • Refined permission checks for project updates, now limited to administrators only.
  • Chores

    • Improved conditional logic for soft deletion of related objects.

Copy link
Contributor

coderabbitai bot commented Oct 7, 2024

Walkthrough

The pull request introduces several enhancements across multiple files, focusing on the functionality of estimate points, project updates, and activity tracking. Key changes include a refined destroy method for handling estimate point deletions and reassignments, stricter permission controls for project updates, and improved logging for issue activities. Additionally, the soft deletion logic is enhanced by checking the on_delete behavior of related fields. Overall, these changes aim to improve the interaction between estimate points and issues, project management, and activity tracking.

Changes

File Path Change Summary
apiserver/plane/app/views/estimate/base.py Updated destroy method to handle new_estimate_id for reassignment and logging of changes.
apiserver/plane/app/views/project/base.py Modified partial_update method to restrict access to ROLE.ADMIN and manage inbox creation.
apiserver/plane/bgtasks/deletion_task.py Enhanced soft_delete_related_objects to check on_delete behavior before soft deletion.
apiserver/plane/bgtasks/issue_activities_task.py Updated activity tracking methods to improve logging clarity and streamline event handling.

Possibly related PRs

Suggested labels

🌟improvement

Suggested reviewers

  • Pablo Hash Escobar
  • Sriram Veeraghanta

Poem

🐇 In the fields where estimates play,
A new path for points has come to stay.
With admins in charge, the projects align,
Soft deletes dance in a logical line.
Issues now track with a clearer view,
Hopping along, we embrace the new! 🌼


Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

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

🧹 Outside diff range and nitpick comments (6)
apiserver/plane/app/views/project/base.py (2)

Line range hint 441-458: New inbox and triage state creation

The method now creates a default inbox and triage state if they don't exist. This is a good addition for consistency in project management.

However, consider adding error handling for the inbox and state creation processes.

Consider wrapping the inbox and state creation in a try-except block:

 if serializer.data["inbox_view"]:
+    try:
         inbox = Inbox.objects.filter(
             project=project,
             is_default=True,
         ).first()
         if not inbox:
             Inbox.objects.create(
                 name=f"{project.name} Inbox",
                 project=project,
                 is_default=True,
             )

         # Create the triage state in Backlog group
         State.objects.get_or_create(
             name="Triage",
             group="triage",
             description="Default state for managing all Inbox Issues",
             project_id=pk,
             color="#ff7700",
             is_triage=True,
         )
+    except Exception as e:
+        return Response(
+            {"error": f"Failed to create inbox or triage state: {str(e)}"},
+            status=status.HTTP_500_INTERNAL_SERVER_ERROR
+        )

Line range hint 460-472: Add logging for new functionality

Consider adding logging statements for the creation of the inbox and triage state. This will help with debugging and monitoring the system's behavior.

Add logging statements after the inbox and state creation:

 if serializer.data["inbox_view"]:
     inbox = Inbox.objects.filter(
         project=project,
         is_default=True,
     ).first()
     if not inbox:
         Inbox.objects.create(
             name=f"{project.name} Inbox",
             project=project,
             is_default=True,
         )
+        logger.info(f"Created default inbox for project {project.id}")

     # Create the triage state in Backlog group
     State.objects.get_or_create(
         name="Triage",
         group="triage",
         description="Default state for managing all Inbox Issues",
         project_id=pk,
         color="#ff7700",
         is_triage=True,
     )
+    logger.info(f"Created or got triage state for project {project.id}")
apiserver/plane/app/views/estimate/base.py (4)

263-287: Optimize Task Queue Processing

Currently, issue_activity.delay() is called within a loop for each issue. If there are many issues, this will enqueue a large number of tasks, which could overwhelm the task processing system.

[performance]

Consider batching the activity logging:

  • Option 1: Modify issue_activity to accept a list of issues and process them in a single task.
  • Option 2: Use group tasks or chunking provided by your task queue (e.g., Celery) to manage the workload efficiently.

This change will improve the scalability and performance of your application.


267-285: Simplify requested_data and current_instance Assignments

The use of complex ternary expressions within the JSON dumps can be simplified for readability.

For example, in requested_data:

"estimate_point": new_estimate_point

Since new_estimate_point is already set appropriately before the loop, you can use it directly.

Similarly, in current_instance:

"estimate_point": str(issue.estimate_point_id) if issue.estimate_point_id else None

This expression is acceptable, but consider assigning it to a variable for clarity:

current_estimate_point = (
    str(issue.estimate_point_id) if issue.estimate_point_id else None
)

And then:

"estimate_point": current_estimate_point

This makes the code cleaner and easier to read.

Also applies to: 298-316


306-316: Time Zone Awareness

When using timezone.now().timestamp(), ensure that the timestamp is time zone aware and consistent across your application.

If your application relies on UTC, you might want to explicitly use timezone.utc or datetime.utcnow().

epoch = int(datetime.datetime.utcnow().timestamp())

Ensure consistency with your application's time zone settings.


Line range hint 232-238: Validate Unique Keys for Estimate Points

In the create and partial_update methods for EstimatePointEndpoint, there is a TODO comment about adding key validation for duplicates.

Implement key uniqueness validation to prevent multiple estimate points with the same key in an estimate.

You can add a validation check:

if EstimatePoint.objects.filter(
    estimate_id=estimate_id,
    key=request.data.get("key")
).exists():
    return Response(
        {"error": "An estimate point with this key already exists."},
        status=status.HTTP_400_BAD_REQUEST,
    )

Would you like assistance in implementing this validation?

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Files that changed from the base of the PR and between 7bb0400 and a65b03b.

📒 Files selected for processing (4)
  • apiserver/plane/app/views/estimate/base.py (3 hunks)
  • apiserver/plane/app/views/project/base.py (1 hunks)
  • apiserver/plane/bgtasks/deletion_task.py (3 hunks)
  • apiserver/plane/bgtasks/issue_activities_task.py (2 hunks)
🧰 Additional context used
🔇 Additional comments (8)
apiserver/plane/bgtasks/deletion_task.py (4)

5-5: Necessary Import of models

The import of models from django.db is essential for accessing models.CASCADE in the updated soft deletion logic below.


22-26: Correct Implementation of on_delete Check

Adding the check for on_delete=models.CASCADE ensures that only related objects intended to be deleted via cascading are soft-deleted. This enhances the accuracy of the soft deletion process and prevents unintended deletions.


27-35: Proper Handling of Related Objects

The code correctly handles both one_to_many and one_to_one relationships when retrieving related objects for soft deletion. This comprehensive approach ensures that all relevant related objects are considered.


37-40: Soft Deleting Related Objects

The loop appropriately sets the deleted_at timestamp for each related object and saves it using the specified database alias. This effectively performs a soft delete on the related objects.

apiserver/plane/app/views/estimate/base.py (3)

263-287: Handle Potential Race Conditions

Updating issues and logging activities separately could lead to race conditions if other processes are modifying the same issues concurrently.

[concurrency]

Consider wrapping the issue updates and activity logging within a transaction to ensure atomicity:

from django.db import transaction

with transaction.atomic():
    # Update issues
    issues.update(estimate_point_id=new_estimate_point)
    # Log activities
    for issue in issues:
        issue_activity.delay(...)

This approach ensures data consistency and integrity.

Also applies to: 296-316


3-6: Unused Imports

Verify whether the imported modules json and timezone are necessary.

  • json: Used in json.dumps() for requested_data and current_instance.
  • timezone: Used for timezone.now().timestamp().

Since both are used in the new code, the imports are appropriate.


Line range hint 340-352: Ensure Deletion of Old Estimate Point Does Not Affect Key Ordering

After deleting the old_estimate_point, you adjust the keys of remaining estimate points. This assumes that the key field is used for ordering and has to be sequential.

Consider whether adjusting the key values is necessary or if it could lead to issues elsewhere:

  • If key is meant to be a unique identifier, changing it might cause confusion.
  • If ordering is important, consider using an explicit ordering field or relying on the database ordering mechanisms.

Verify the impact of this change on other parts of the application.

apiserver/plane/bgtasks/issue_activities_task.py (1)

468-468: Correctly set verb based on estimate point changes

The change appropriately sets the verb to "removed" when new_estimate is None, accurately reflecting the removal of an estimate point. This ensures that activity logging remains precise.

@@ -413,7 +413,7 @@ def create(self, request, slug):
status=status.HTTP_410_GONE,
)

@allow_permission([ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE")
@allow_permission([ROLE.ADMIN])
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

Restricted access to ADMIN role only

The partial_update method now only allows ADMIN role access. This change enhances security but may impact existing workflows where MEMBER roles previously had update permissions.

Please ensure this change aligns with your project's access control requirements and update any related documentation or user guides accordingly.

@@ -154,8 +163,7 @@ def hard_delete():
if hasattr(model, "deleted_at"):
# Get all instances where 'deleted_at' is greater than 30 days ago
_ = model.all_objects.filter(
deleted_at__lt=timezone.now()
- timezone.timedelta(days=days)
deleted_at__lt=timezone.now() - timezone.timedelta(days=days)
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Consider Consolidating Hard Deletion Logic

In the hard_delete function, you individually delete instances of specific models and then iterate over all models with a deleted_at field to delete any remaining instances. This could lead to redundant deletion operations and impact performance.

Suggestion:

Evaluate whether the individual deletions of specific models are necessary if the final loop sufficiently handles the hard deletion of all models with a deleted_at field. Consolidating the deletion logic might improve performance and simplify maintenance.

Comment on lines +257 to +261
issues = Issue.objects.filter(
project_id=project_id,
workspace__slug=slug,
estimate_point_id=estimate_point_id,
)
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

Ensure Issues are Updated to Remove Deleted Estimate Point

In the else block where new_estimate_id is not provided, the issues are not updated to remove the reference to the deleted estimate_point_id. This means the issues will still reference an estimate point that no longer exists, which could lead to integrity issues.

To fix this, add an issues.update(estimate_point_id=None) after the loop in the else block to set the estimate_point_id to None for all affected issues:

issues.update(estimate_point_id=None)

This ensures that all issues previously associated with the deleted estimate point no longer hold invalid references.

Comment on lines +257 to +316
issues = Issue.objects.filter(
project_id=project_id,
workspace__slug=slug,
estimate_point_id=estimate_point_id,
)
for issue in issues:
issue_activity.delay(
type="issue.activity.updated",
requested_data=json.dumps(
{
"estimate_point": (
str(new_estimate_id)
if new_estimate_id
else None
),
}
),
actor_id=str(request.user.id),
issue_id=issue.id,
project_id=str(project_id),
current_instance=json.dumps(
{
"estimate_point": (
str(issue.estimate_point_id)
if issue.estimate_point_id
else None
),
}
),
epoch=int(timezone.now().timestamp()),
)
issues.update(estimate_point_id=new_estimate_id)
else:
issues = Issue.objects.filter(
project_id=project_id,
workspace__slug=slug,
estimate_point_id=estimate_point_id,
).update(estimate_point_id=new_estimate_id)
)
for issue in issues:
issue_activity.delay(
type="issue.activity.updated",
requested_data=json.dumps(
{
"estimate_point": None,
}
),
actor_id=str(request.user.id),
issue_id=issue.id,
project_id=str(project_id),
current_instance=json.dumps(
{
"estimate_point": (
str(issue.estimate_point_id)
if issue.estimate_point_id
else None
),
}
),
epoch=int(timezone.now().timestamp()),
)
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Refactor Repetitive Code in destroy Method

The code inside the if new_estimate_id and else blocks shares significant similarities, particularly in how issues are retrieved and how issue_activity.delay is called. This duplication can make the code harder to maintain and more prone to errors.

You can refactor the code to eliminate duplication:

issues = Issue.objects.filter(
    project_id=project_id,
    workspace__slug=slug,
    estimate_point_id=estimate_point_id,
)

new_estimate_point = str(new_estimate_id) if new_estimate_id else None

for issue in issues:
    issue_activity.delay(
        type="issue.activity.updated",
        requested_data=json.dumps({"estimate_point": new_estimate_point}),
        actor_id=str(request.user.id),
        issue_id=issue.id,
        project_id=str(project_id),
        current_instance=json.dumps(
            {
                "estimate_point": (
                    str(issue.estimate_point_id)
                    if issue.estimate_point_id
                    else None
                )
            }
        ),
        epoch=int(timezone.now().timestamp()),
    )

if new_estimate_id:
    issues.update(estimate_point_id=new_estimate_id)
else:
    issues.update(estimate_point_id=None)

This refactoring reduces code duplication and makes the logic clearer.

),
epoch=int(timezone.now().timestamp()),
)
issues.update(estimate_point_id=new_estimate_id)
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

Consistent Issue Updates

Ensure that the issues.update(...) method is called in both the if and else blocks and placed outside the loop. In the current code, the update in the else block is missing.

Add the following after line 316:

issues.update(estimate_point_id=None)

This ensures that all issues have their estimate_point_id set to None when no new_estimate_id is provided.

Also applies to: 317-317

Comment on lines +1703 to +1708
else "inbox_issue" if inbox else "issue"
),
event_id=(
activity.issue_comment_id
if activity.field == "comment"
else inbox
if inbox
else activity.issue_id
else inbox if inbox else activity.issue_id
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Refactor nested ternary operators for improved readability

The nested ternary operators in the assignments of event and event_id reduce code readability and can make maintenance challenging. Consider refactoring the code to enhance clarity.

Apply this diff to improve readability:

             if len(issue_activities_created):
                 for activity in issue_activities_created:
+                    if activity.field == "comment":
+                        event = "issue_comment"
+                        event_id = activity.issue_comment_id
+                    elif inbox:
+                        event = "inbox_issue"
+                        event_id = inbox
+                    else:
+                        event = "issue"
+                        event_id = activity.issue_id
                     webhook_activity.delay(
-                        event=(
-                            "issue_comment"
-                            if activity.field == "comment"
-                            else "inbox_issue" if inbox else "issue"
-                        ),
-                        event_id=(
-                            activity.issue_comment_id
-                            if activity.field == "comment"
-                            else inbox if inbox else activity.issue_id
-                        ),
+                        event=event,
+                        event_id=event_id,
                         verb=activity.verb,
                         field=(
                             "description"
                             if activity.field == "comment"
                             else activity.field
                         ),
                         old_value=(
                             activity.old_value
                             if activity.old_value != ""
                             else None
                         ),
                         new_value=(
                             activity.new_value
                             if activity.new_value != ""
                             else None
                         ),
                         actor_id=activity.actor_id,
                         current_site=origin,
                         slug=activity.workspace.slug,
                         old_identifier=activity.old_identifier,
                         new_identifier=activity.new_identifier,
                     )

This refactoring assigns event and event_id before the webhook_activity.delay call, enhancing the readability and maintainability of the code.

📝 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
else "inbox_issue" if inbox else "issue"
),
event_id=(
activity.issue_comment_id
if activity.field == "comment"
else inbox
if inbox
else activity.issue_id
else inbox if inbox else activity.issue_id
if activity.field == "comment":
event = "issue_comment"
event_id = activity.issue_comment_id
elif inbox:
event = "inbox_issue"
event_id = inbox
else:
event = "issue"
event_id = activity.issue_id
webhook_activity.delay(
event=event,
event_id=event_id,
verb=activity.verb,
field=(
"description"
if activity.field == "comment"
else activity.field
),
old_value=(
activity.old_value
if activity.old_value != ""
else None
),
new_value=(
activity.new_value
if activity.new_value != ""
else None
),
actor_id=activity.actor_id,
current_site=origin,
slug=activity.workspace.slug,
old_identifier=activity.old_identifier,
new_identifier=activity.new_identifier,
)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
⚙️backend 🐛bug Something isn't working
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants