fix: error handling for db based integrity errors (#6632)

* fix: error handling for db based integrity errors

* fix: meta endpoint to return correct error message

* fix: module activity
This commit is contained in:
Nikhil 2025-02-19 02:04:28 +05:30 committed by GitHub
parent db4ecee475
commit d3af913ec7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 250 additions and 167 deletions

View file

@ -1,6 +1,7 @@
# Django imports # Django imports
from django.utils import timezone from django.utils import timezone
from lxml import html from lxml import html
from django.db import IntegrityError
# Third party imports # Third party imports
from rest_framework import serializers from rest_framework import serializers
@ -138,6 +139,7 @@ class IssueSerializer(BaseSerializer):
updated_by_id = issue.updated_by_id updated_by_id = issue.updated_by_id
if assignees is not None and len(assignees): if assignees is not None and len(assignees):
try:
IssueAssignee.objects.bulk_create( IssueAssignee.objects.bulk_create(
[ [
IssueAssignee( IssueAssignee(
@ -152,7 +154,10 @@ class IssueSerializer(BaseSerializer):
], ],
batch_size=10, batch_size=10,
) )
except IntegrityError:
pass
else: else:
try:
# Then assign it to default assignee # Then assign it to default assignee
if default_assignee_id is not None: if default_assignee_id is not None:
IssueAssignee.objects.create( IssueAssignee.objects.create(
@ -163,8 +168,11 @@ class IssueSerializer(BaseSerializer):
created_by_id=created_by_id, created_by_id=created_by_id,
updated_by_id=updated_by_id, updated_by_id=updated_by_id,
) )
except IntegrityError:
pass
if labels is not None and len(labels): if labels is not None and len(labels):
try:
IssueLabel.objects.bulk_create( IssueLabel.objects.bulk_create(
[ [
IssueLabel( IssueLabel(
@ -179,6 +187,8 @@ class IssueSerializer(BaseSerializer):
], ],
batch_size=10, batch_size=10,
) )
except IntegrityError:
pass
return issue return issue
@ -194,6 +204,7 @@ class IssueSerializer(BaseSerializer):
if assignees is not None: if assignees is not None:
IssueAssignee.objects.filter(issue=instance).delete() IssueAssignee.objects.filter(issue=instance).delete()
try:
IssueAssignee.objects.bulk_create( IssueAssignee.objects.bulk_create(
[ [
IssueAssignee( IssueAssignee(
@ -209,9 +220,12 @@ class IssueSerializer(BaseSerializer):
batch_size=10, batch_size=10,
ignore_conflicts=True, ignore_conflicts=True,
) )
except IntegrityError:
pass
if labels is not None: if labels is not None:
IssueLabel.objects.filter(issue=instance).delete() IssueLabel.objects.filter(issue=instance).delete()
try:
IssueLabel.objects.bulk_create( IssueLabel.objects.bulk_create(
[ [
IssueLabel( IssueLabel(
@ -227,6 +241,8 @@ class IssueSerializer(BaseSerializer):
batch_size=10, batch_size=10,
ignore_conflicts=True, ignore_conflicts=True,
) )
except IntegrityError:
pass
# Time updation occues even when other related models are updated # Time updation occues even when other related models are updated
instance.updated_at = timezone.now() instance.updated_at = timezone.now()

View file

@ -2,6 +2,7 @@
from django.utils import timezone from django.utils import timezone
from django.core.validators import URLValidator from django.core.validators import URLValidator
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.db import IntegrityError
# Third Party imports # Third Party imports
from rest_framework import serializers from rest_framework import serializers
@ -134,6 +135,7 @@ class IssueCreateSerializer(BaseSerializer):
updated_by_id = issue.updated_by_id updated_by_id = issue.updated_by_id
if assignees is not None and len(assignees): if assignees is not None and len(assignees):
try:
IssueAssignee.objects.bulk_create( IssueAssignee.objects.bulk_create(
[ [
IssueAssignee( IssueAssignee(
@ -148,9 +150,12 @@ class IssueCreateSerializer(BaseSerializer):
], ],
batch_size=10, batch_size=10,
) )
except IntegrityError:
pass
else: else:
# Then assign it to default assignee # Then assign it to default assignee
if default_assignee_id is not None: if default_assignee_id is not None:
try:
IssueAssignee.objects.create( IssueAssignee.objects.create(
assignee_id=default_assignee_id, assignee_id=default_assignee_id,
issue=issue, issue=issue,
@ -159,8 +164,11 @@ class IssueCreateSerializer(BaseSerializer):
created_by_id=created_by_id, created_by_id=created_by_id,
updated_by_id=updated_by_id, updated_by_id=updated_by_id,
) )
except IntegrityError:
pass
if labels is not None and len(labels): if labels is not None and len(labels):
try:
IssueLabel.objects.bulk_create( IssueLabel.objects.bulk_create(
[ [
IssueLabel( IssueLabel(
@ -175,6 +183,8 @@ class IssueCreateSerializer(BaseSerializer):
], ],
batch_size=10, batch_size=10,
) )
except IntegrityError:
pass
return issue return issue
@ -190,6 +200,7 @@ class IssueCreateSerializer(BaseSerializer):
if assignees is not None: if assignees is not None:
IssueAssignee.objects.filter(issue=instance).delete() IssueAssignee.objects.filter(issue=instance).delete()
try:
IssueAssignee.objects.bulk_create( IssueAssignee.objects.bulk_create(
[ [
IssueAssignee( IssueAssignee(
@ -205,9 +216,12 @@ class IssueCreateSerializer(BaseSerializer):
batch_size=10, batch_size=10,
ignore_conflicts=True, ignore_conflicts=True,
) )
except IntegrityError:
pass
if labels is not None: if labels is not None:
IssueLabel.objects.filter(issue=instance).delete() IssueLabel.objects.filter(issue=instance).delete()
try:
IssueLabel.objects.bulk_create( IssueLabel.objects.bulk_create(
[ [
IssueLabel( IssueLabel(
@ -223,6 +237,8 @@ class IssueCreateSerializer(BaseSerializer):
batch_size=10, batch_size=10,
ignore_conflicts=True, ignore_conflicts=True,
) )
except IntegrityError:
pass
# Time updation occues even when other related models are updated # Time updation occues even when other related models are updated
instance.updated_at = timezone.now() instance.updated_at = timezone.now()

View file

@ -5,6 +5,7 @@ import uuid
from django.conf import settings from django.conf import settings
from django.http import HttpResponseRedirect from django.http import HttpResponseRedirect
from django.utils import timezone from django.utils import timezone
from django.db import IntegrityError
# Third party imports # Third party imports
from rest_framework import status from rest_framework import status
@ -679,15 +680,30 @@ class ProjectBulkAssetEndpoint(BaseAPIView):
[self.save_project_cover(asset, project_id) for asset in assets] [self.save_project_cover(asset, project_id) for asset in assets]
if asset.entity_type == FileAsset.EntityTypeContext.ISSUE_DESCRIPTION: if asset.entity_type == FileAsset.EntityTypeContext.ISSUE_DESCRIPTION:
# For some cases, the bulk api is called after the issue is deleted creating
# an integrity error
try:
assets.update(issue_id=entity_id) assets.update(issue_id=entity_id)
except IntegrityError:
pass
if asset.entity_type == FileAsset.EntityTypeContext.COMMENT_DESCRIPTION: if asset.entity_type == FileAsset.EntityTypeContext.COMMENT_DESCRIPTION:
# For some cases, the bulk api is called after the comment is deleted
# creating an integrity error
try:
assets.update(comment_id=entity_id) assets.update(comment_id=entity_id)
except IntegrityError:
pass
if asset.entity_type == FileAsset.EntityTypeContext.PAGE_DESCRIPTION: if asset.entity_type == FileAsset.EntityTypeContext.PAGE_DESCRIPTION:
assets.update(page_id=entity_id) assets.update(page_id=entity_id)
if asset.entity_type == FileAsset.EntityTypeContext.DRAFT_ISSUE_DESCRIPTION: if asset.entity_type == FileAsset.EntityTypeContext.DRAFT_ISSUE_DESCRIPTION:
# For some cases, the bulk api is called after the draft issue is deleted
# creating an integrity error
try:
assets.update(draft_issue_id=entity_id) assets.update(draft_issue_id=entity_id)
except IntegrityError:
pass
return Response(status=status.HTTP_204_NO_CONTENT) return Response(status=status.HTTP_204_NO_CONTENT)

View file

@ -5,6 +5,7 @@ import json
from django.utils import timezone from django.utils import timezone
from django.db.models import Exists from django.db.models import Exists
from django.core.serializers.json import DjangoJSONEncoder from django.core.serializers.json import DjangoJSONEncoder
from django.db import IntegrityError
# Third Party imports # Third Party imports
from rest_framework.response import Response from rest_framework.response import Response
@ -164,10 +165,13 @@ class CommentReactionViewSet(BaseViewSet):
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST]) @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
def create(self, request, slug, project_id, comment_id): def create(self, request, slug, project_id, comment_id):
try:
serializer = CommentReactionSerializer(data=request.data) serializer = CommentReactionSerializer(data=request.data)
if serializer.is_valid(): if serializer.is_valid():
serializer.save( serializer.save(
project_id=project_id, actor_id=request.user.id, comment_id=comment_id project_id=project_id,
actor_id=request.user.id,
comment_id=comment_id,
) )
issue_activity.delay( issue_activity.delay(
type="comment_reaction.activity.created", type="comment_reaction.activity.created",
@ -182,6 +186,11 @@ class CommentReactionViewSet(BaseViewSet):
) )
return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
except IntegrityError:
return Response(
{"error": "Reaction already exists for the user"},
status=status.HTTP_400_BAD_REQUEST,
)
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST]) @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
def destroy(self, request, slug, project_id, comment_id, reaction_code): def destroy(self, request, slug, project_id, comment_id, reaction_code):

View file

@ -55,6 +55,20 @@ class LabelViewSet(BaseViewSet):
@invalidate_cache(path="/api/workspaces/:slug/labels/", url_params=True, user=False) @invalidate_cache(path="/api/workspaces/:slug/labels/", url_params=True, user=False)
@allow_permission([ROLE.ADMIN]) @allow_permission([ROLE.ADMIN])
def partial_update(self, request, *args, **kwargs): def partial_update(self, request, *args, **kwargs):
# Check if the label name is unique within the project
if (
"name" in request.data
and Label.objects.filter(
project_id=kwargs["project_id"], name=request.data["name"]
)
.exclude(pk=kwargs["pk"])
.exists()
):
return Response(
{"error": "Label with the same name already exists in the project"},
status=status.HTTP_400_BAD_REQUEST,
)
# call the parent method to perform the update
return super().partial_update(request, *args, **kwargs) return super().partial_update(request, *args, **kwargs)
@invalidate_cache(path="/api/workspaces/:slug/labels/", url_params=True, user=False) @invalidate_cache(path="/api/workspaces/:slug/labels/", url_params=True, user=False)

View file

@ -4,6 +4,7 @@ from rest_framework.response import Response
# Django modules # Django modules
from django.db.models import Q from django.db.models import Q
from django.db import IntegrityError
# Module imports # Module imports
from plane.app.views.base import BaseAPIView from plane.app.views.base import BaseAPIView
@ -31,6 +32,7 @@ class WorkspaceFavoriteEndpoint(BaseAPIView):
@allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE") @allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE")
def post(self, request, slug): def post(self, request, slug):
try:
workspace = Workspace.objects.get(slug=slug) workspace = Workspace.objects.get(slug=slug)
serializer = UserFavoriteSerializer(data=request.data) serializer = UserFavoriteSerializer(data=request.data)
if serializer.is_valid(): if serializer.is_valid():
@ -41,6 +43,10 @@ class WorkspaceFavoriteEndpoint(BaseAPIView):
) )
return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
except IntegrityError:
return Response(
{"error": "Favorite already exists"}, status=status.HTTP_400_BAD_REQUEST
)
@allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE") @allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE")
def patch(self, request, slug, favorite_id): def patch(self, request, slug, favorite_id):

View file

@ -790,14 +790,15 @@ def create_cycle_issue_activity(
issue_id=updated_record.get("issue_id"), issue_id=updated_record.get("issue_id"),
actor_id=actor_id, actor_id=actor_id,
verb="updated", verb="updated",
old_value=old_cycle.name, old_value=old_cycle.name if old_cycle else "",
new_value=new_cycle.name, new_value=new_cycle.name if new_cycle else "",
field="cycles", field="cycles",
project_id=project_id, project_id=project_id,
workspace_id=workspace_id, workspace_id=workspace_id,
comment=f"updated cycle from {old_cycle.name} to {new_cycle.name}", comment=f"""updated cycle from {old_cycle.name if old_cycle else ""}
old_identifier=old_cycle.id, to {new_cycle.name if new_cycle else ""}""",
new_identifier=new_cycle.id, old_identifier=old_cycle.id if old_cycle else None,
new_identifier=new_cycle.id if new_cycle else None,
epoch=epoch, epoch=epoch,
) )
) )
@ -893,11 +894,11 @@ def create_module_issue_activity(
actor_id=actor_id, actor_id=actor_id,
verb="created", verb="created",
old_value="", old_value="",
new_value=module.name, new_value=module.name if module else "",
field="modules", field="modules",
project_id=project_id, project_id=project_id,
workspace_id=workspace_id, workspace_id=workspace_id,
comment=f"added module {module.name}", comment=f"added module {module.name if module else ''}",
new_identifier=requested_data.get("module_id"), new_identifier=requested_data.get("module_id"),
epoch=epoch, epoch=epoch,
) )
@ -1413,7 +1414,7 @@ def delete_issue_relation_activity(
), ),
project_id=project_id, project_id=project_id,
workspace_id=workspace_id, workspace_id=workspace_id,
comment=f'deleted {requested_data.get("relation_type")} relation', comment=f"deleted {requested_data.get('relation_type')} relation",
old_identifier=requested_data.get("related_issue"), old_identifier=requested_data.get("related_issue"),
epoch=epoch, epoch=epoch,
) )

View file

@ -1,5 +1,6 @@
# Python imports # Python imports
from django.utils import timezone from django.utils import timezone
from django.db import DatabaseError
# Third party imports # Third party imports
from celery import shared_task from celery import shared_task
@ -22,8 +23,12 @@ def recent_visited_task(entity_name, entity_identifier, user_id, project_id, slu
).first() ).first()
if recent_visited: if recent_visited:
# Check if the database is available
try:
recent_visited.visited_at = timezone.now() recent_visited.visited_at = timezone.now()
recent_visited.save(update_fields=["visited_at"]) recent_visited.save(update_fields=["visited_at"])
except DatabaseError:
pass
else: else:
recent_visited_count = UserRecentVisit.objects.filter( recent_visited_count = UserRecentVisit.objects.filter(
user_id=user_id, workspace_id=workspace.id user_id=user_id, workspace_id=workspace.id

View file

@ -14,9 +14,9 @@ class ProjectMetaDataEndpoint(BaseAPIView):
def get(self, request, anchor): def get(self, request, anchor):
try: try:
deploy_board = DeployBoard.objects.filter( deploy_board = DeployBoard.objects.get(
anchor=anchor, entity_name="project" anchor=anchor, entity_name="project"
).first() )
except DeployBoard.DoesNotExist: except DeployBoard.DoesNotExist:
return Response( return Response(
{"error": "Project is not published"}, status=status.HTTP_404_NOT_FOUND {"error": "Project is not published"}, status=status.HTTP_404_NOT_FOUND