diff --git a/apiserver/plane/api/serializers/issue.py b/apiserver/plane/api/serializers/issue.py index ced1c9b92c6..275ebeb0760 100644 --- a/apiserver/plane/api/serializers/issue.py +++ b/apiserver/plane/api/serializers/issue.py @@ -1,6 +1,7 @@ # Django imports from django.utils import timezone from lxml import html +from django.db import IntegrityError # Third party imports from rest_framework import serializers @@ -138,47 +139,56 @@ def create(self, validated_data): updated_by_id = issue.updated_by_id if assignees is not None and len(assignees): - IssueAssignee.objects.bulk_create( - [ - IssueAssignee( - assignee_id=assignee_id, + try: + IssueAssignee.objects.bulk_create( + [ + IssueAssignee( + assignee_id=assignee_id, + issue=issue, + project_id=project_id, + workspace_id=workspace_id, + created_by_id=created_by_id, + updated_by_id=updated_by_id, + ) + for assignee_id in assignees + ], + batch_size=10, + ) + except IntegrityError: + pass + else: + try: + # Then assign it to default assignee + if default_assignee_id is not None: + IssueAssignee.objects.create( + assignee_id=default_assignee_id, issue=issue, project_id=project_id, workspace_id=workspace_id, created_by_id=created_by_id, updated_by_id=updated_by_id, ) - for assignee_id in assignees - ], - batch_size=10, - ) - else: - # Then assign it to default assignee - if default_assignee_id is not None: - IssueAssignee.objects.create( - assignee_id=default_assignee_id, - issue=issue, - project_id=project_id, - workspace_id=workspace_id, - created_by_id=created_by_id, - updated_by_id=updated_by_id, - ) + except IntegrityError: + pass if labels is not None and len(labels): - IssueLabel.objects.bulk_create( - [ - IssueLabel( - label_id=label_id, - issue=issue, - project_id=project_id, - workspace_id=workspace_id, - created_by_id=created_by_id, - updated_by_id=updated_by_id, - ) - for label_id in labels - ], - batch_size=10, - ) + try: + IssueLabel.objects.bulk_create( + [ + IssueLabel( + label_id=label_id, + issue=issue, + project_id=project_id, + workspace_id=workspace_id, + created_by_id=created_by_id, + updated_by_id=updated_by_id, + ) + for label_id in labels + ], + batch_size=10, + ) + except IntegrityError: + pass return issue @@ -194,39 +204,45 @@ def update(self, instance, validated_data): if assignees is not None: IssueAssignee.objects.filter(issue=instance).delete() - IssueAssignee.objects.bulk_create( - [ - IssueAssignee( - assignee_id=assignee_id, - issue=instance, - project_id=project_id, - workspace_id=workspace_id, - created_by_id=created_by_id, - updated_by_id=updated_by_id, - ) - for assignee_id in assignees - ], - batch_size=10, - ignore_conflicts=True, - ) + try: + IssueAssignee.objects.bulk_create( + [ + IssueAssignee( + assignee_id=assignee_id, + issue=instance, + project_id=project_id, + workspace_id=workspace_id, + created_by_id=created_by_id, + updated_by_id=updated_by_id, + ) + for assignee_id in assignees + ], + batch_size=10, + ignore_conflicts=True, + ) + except IntegrityError: + pass if labels is not None: IssueLabel.objects.filter(issue=instance).delete() - IssueLabel.objects.bulk_create( - [ - IssueLabel( - label_id=label_id, - issue=instance, - project_id=project_id, - workspace_id=workspace_id, - created_by_id=created_by_id, - updated_by_id=updated_by_id, - ) - for label_id in labels - ], - batch_size=10, - ignore_conflicts=True, - ) + try: + IssueLabel.objects.bulk_create( + [ + IssueLabel( + label_id=label_id, + issue=instance, + project_id=project_id, + workspace_id=workspace_id, + created_by_id=created_by_id, + updated_by_id=updated_by_id, + ) + for label_id in labels + ], + batch_size=10, + ignore_conflicts=True, + ) + except IntegrityError: + pass # Time updation occues even when other related models are updated instance.updated_at = timezone.now() diff --git a/apiserver/plane/app/serializers/issue.py b/apiserver/plane/app/serializers/issue.py index a1707ea99c4..a62b266e999 100644 --- a/apiserver/plane/app/serializers/issue.py +++ b/apiserver/plane/app/serializers/issue.py @@ -2,6 +2,7 @@ from django.utils import timezone from django.core.validators import URLValidator from django.core.exceptions import ValidationError +from django.db import IntegrityError # Third Party imports from rest_framework import serializers @@ -134,47 +135,56 @@ def create(self, validated_data): updated_by_id = issue.updated_by_id if assignees is not None and len(assignees): - IssueAssignee.objects.bulk_create( - [ - IssueAssignee( - assignee=user, - issue=issue, - project_id=project_id, - workspace_id=workspace_id, - created_by_id=created_by_id, - updated_by_id=updated_by_id, - ) - for user in assignees - ], - batch_size=10, - ) + try: + IssueAssignee.objects.bulk_create( + [ + IssueAssignee( + assignee=user, + issue=issue, + project_id=project_id, + workspace_id=workspace_id, + created_by_id=created_by_id, + updated_by_id=updated_by_id, + ) + for user in assignees + ], + batch_size=10, + ) + except IntegrityError: + pass else: # Then assign it to default assignee if default_assignee_id is not None: - IssueAssignee.objects.create( - assignee_id=default_assignee_id, - issue=issue, - project_id=project_id, - workspace_id=workspace_id, - created_by_id=created_by_id, - updated_by_id=updated_by_id, - ) - - if labels is not None and len(labels): - IssueLabel.objects.bulk_create( - [ - IssueLabel( - label=label, + try: + IssueAssignee.objects.create( + assignee_id=default_assignee_id, issue=issue, project_id=project_id, workspace_id=workspace_id, created_by_id=created_by_id, updated_by_id=updated_by_id, ) - for label in labels - ], - batch_size=10, - ) + except IntegrityError: + pass + + if labels is not None and len(labels): + try: + IssueLabel.objects.bulk_create( + [ + IssueLabel( + label=label, + issue=issue, + project_id=project_id, + workspace_id=workspace_id, + created_by_id=created_by_id, + updated_by_id=updated_by_id, + ) + for label in labels + ], + batch_size=10, + ) + except IntegrityError: + pass return issue @@ -190,39 +200,45 @@ def update(self, instance, validated_data): if assignees is not None: IssueAssignee.objects.filter(issue=instance).delete() - IssueAssignee.objects.bulk_create( - [ - IssueAssignee( - assignee=user, - issue=instance, - project_id=project_id, - workspace_id=workspace_id, - created_by_id=created_by_id, - updated_by_id=updated_by_id, - ) - for user in assignees - ], - batch_size=10, - ignore_conflicts=True, - ) + try: + IssueAssignee.objects.bulk_create( + [ + IssueAssignee( + assignee=user, + issue=instance, + project_id=project_id, + workspace_id=workspace_id, + created_by_id=created_by_id, + updated_by_id=updated_by_id, + ) + for user in assignees + ], + batch_size=10, + ignore_conflicts=True, + ) + except IntegrityError: + pass if labels is not None: IssueLabel.objects.filter(issue=instance).delete() - IssueLabel.objects.bulk_create( - [ - IssueLabel( - label=label, - issue=instance, - project_id=project_id, - workspace_id=workspace_id, - created_by_id=created_by_id, - updated_by_id=updated_by_id, - ) - for label in labels - ], - batch_size=10, - ignore_conflicts=True, - ) + try: + IssueLabel.objects.bulk_create( + [ + IssueLabel( + label=label, + issue=instance, + project_id=project_id, + workspace_id=workspace_id, + created_by_id=created_by_id, + updated_by_id=updated_by_id, + ) + for label in labels + ], + batch_size=10, + ignore_conflicts=True, + ) + except IntegrityError: + pass # Time updation occues even when other related models are updated instance.updated_at = timezone.now() diff --git a/apiserver/plane/app/views/asset/v2.py b/apiserver/plane/app/views/asset/v2.py index 3c1442022fc..da36b91a0ac 100644 --- a/apiserver/plane/app/views/asset/v2.py +++ b/apiserver/plane/app/views/asset/v2.py @@ -5,6 +5,7 @@ from django.conf import settings from django.http import HttpResponseRedirect from django.utils import timezone +from django.db import IntegrityError # Third party imports from rest_framework import status @@ -679,15 +680,30 @@ def post(self, request, slug, project_id, entity_id): [self.save_project_cover(asset, project_id) for asset in assets] if asset.entity_type == FileAsset.EntityTypeContext.ISSUE_DESCRIPTION: - assets.update(issue_id=entity_id) + # For some cases, the bulk api is called after the issue is deleted creating + # an integrity error + try: + assets.update(issue_id=entity_id) + except IntegrityError: + pass if asset.entity_type == FileAsset.EntityTypeContext.COMMENT_DESCRIPTION: - assets.update(comment_id=entity_id) + # For some cases, the bulk api is called after the comment is deleted + # creating an integrity error + try: + assets.update(comment_id=entity_id) + except IntegrityError: + pass if asset.entity_type == FileAsset.EntityTypeContext.PAGE_DESCRIPTION: assets.update(page_id=entity_id) if asset.entity_type == FileAsset.EntityTypeContext.DRAFT_ISSUE_DESCRIPTION: - assets.update(draft_issue_id=entity_id) + # 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) + except IntegrityError: + pass return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/apiserver/plane/app/views/issue/comment.py b/apiserver/plane/app/views/issue/comment.py index d072bb88112..91d27bff29e 100644 --- a/apiserver/plane/app/views/issue/comment.py +++ b/apiserver/plane/app/views/issue/comment.py @@ -5,6 +5,7 @@ from django.utils import timezone from django.db.models import Exists from django.core.serializers.json import DjangoJSONEncoder +from django.db import IntegrityError # Third Party imports from rest_framework.response import Response @@ -164,24 +165,32 @@ def get_queryset(self): @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST]) def create(self, request, slug, project_id, comment_id): - serializer = CommentReactionSerializer(data=request.data) - if serializer.is_valid(): - serializer.save( - project_id=project_id, actor_id=request.user.id, comment_id=comment_id - ) - issue_activity.delay( - type="comment_reaction.activity.created", - requested_data=json.dumps(request.data, cls=DjangoJSONEncoder), - actor_id=str(request.user.id), - issue_id=None, - project_id=str(project_id), - current_instance=None, - epoch=int(timezone.now().timestamp()), - notification=True, - origin=request.META.get("HTTP_ORIGIN"), + try: + serializer = CommentReactionSerializer(data=request.data) + if serializer.is_valid(): + serializer.save( + project_id=project_id, + actor_id=request.user.id, + comment_id=comment_id, + ) + issue_activity.delay( + type="comment_reaction.activity.created", + requested_data=json.dumps(request.data, cls=DjangoJSONEncoder), + actor_id=str(request.user.id), + issue_id=None, + project_id=str(project_id), + current_instance=None, + epoch=int(timezone.now().timestamp()), + notification=True, + origin=request.META.get("HTTP_ORIGIN"), + ) + return Response(serializer.data, status=status.HTTP_201_CREATED) + 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, ) - return Response(serializer.data, status=status.HTTP_201_CREATED) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST]) def destroy(self, request, slug, project_id, comment_id, reaction_code): diff --git a/apiserver/plane/app/views/issue/label.py b/apiserver/plane/app/views/issue/label.py index afecf790ab8..79a8a777031 100644 --- a/apiserver/plane/app/views/issue/label.py +++ b/apiserver/plane/app/views/issue/label.py @@ -55,6 +55,20 @@ def create(self, request, slug, project_id): @invalidate_cache(path="/api/workspaces/:slug/labels/", url_params=True, user=False) @allow_permission([ROLE.ADMIN]) 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) @invalidate_cache(path="/api/workspaces/:slug/labels/", url_params=True, user=False) @@ -74,7 +88,7 @@ def post(self, request, slug, project_id): Label( name=label.get("name", "Migrated"), description=label.get("description", "Migrated Issue"), - color=f"#{random.randint(0, 0xFFFFFF+1):06X}", + color=f"#{random.randint(0, 0xFFFFFF + 1):06X}", project_id=project_id, workspace_id=project.workspace_id, created_by=request.user, diff --git a/apiserver/plane/app/views/workspace/favorite.py b/apiserver/plane/app/views/workspace/favorite.py index 38fa1bdefa8..055f4d67879 100644 --- a/apiserver/plane/app/views/workspace/favorite.py +++ b/apiserver/plane/app/views/workspace/favorite.py @@ -4,6 +4,7 @@ # Django modules from django.db.models import Q +from django.db import IntegrityError # Module imports from plane.app.views.base import BaseAPIView @@ -31,16 +32,21 @@ def get(self, request, slug): @allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE") def post(self, request, slug): - workspace = Workspace.objects.get(slug=slug) - serializer = UserFavoriteSerializer(data=request.data) - if serializer.is_valid(): - serializer.save( - user_id=request.user.id, - workspace=workspace, - project_id=request.data.get("project_id", None), + try: + workspace = Workspace.objects.get(slug=slug) + serializer = UserFavoriteSerializer(data=request.data) + if serializer.is_valid(): + serializer.save( + user_id=request.user.id, + workspace=workspace, + project_id=request.data.get("project_id", None), + ) + return Response(serializer.data, status=status.HTTP_200_OK) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + except IntegrityError: + return Response( + {"error": "Favorite already exists"}, status=status.HTTP_400_BAD_REQUEST ) - return Response(serializer.data, status=status.HTTP_200_OK) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) @allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE") def patch(self, request, slug, favorite_id): diff --git a/apiserver/plane/bgtasks/issue_activities_task.py b/apiserver/plane/bgtasks/issue_activities_task.py index 2113819797e..1bc4817f872 100644 --- a/apiserver/plane/bgtasks/issue_activities_task.py +++ b/apiserver/plane/bgtasks/issue_activities_task.py @@ -790,14 +790,15 @@ def create_cycle_issue_activity( issue_id=updated_record.get("issue_id"), actor_id=actor_id, verb="updated", - old_value=old_cycle.name, - new_value=new_cycle.name, + old_value=old_cycle.name if old_cycle else "", + new_value=new_cycle.name if new_cycle else "", field="cycles", project_id=project_id, workspace_id=workspace_id, - comment=f"updated cycle from {old_cycle.name} to {new_cycle.name}", - old_identifier=old_cycle.id, - new_identifier=new_cycle.id, + comment=f"""updated cycle from {old_cycle.name if old_cycle else ""} + to {new_cycle.name if new_cycle else ""}""", + old_identifier=old_cycle.id if old_cycle else None, + new_identifier=new_cycle.id if new_cycle else None, epoch=epoch, ) ) @@ -893,11 +894,11 @@ def create_module_issue_activity( actor_id=actor_id, verb="created", old_value="", - new_value=module.name, + new_value=module.name if module else "", field="modules", project_id=project_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"), epoch=epoch, ) @@ -1413,7 +1414,7 @@ def delete_issue_relation_activity( ), project_id=project_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"), epoch=epoch, ) diff --git a/apiserver/plane/bgtasks/recent_visited_task.py b/apiserver/plane/bgtasks/recent_visited_task.py index e8e3eb60fe2..4203867da7f 100644 --- a/apiserver/plane/bgtasks/recent_visited_task.py +++ b/apiserver/plane/bgtasks/recent_visited_task.py @@ -1,5 +1,6 @@ # Python imports from django.utils import timezone +from django.db import DatabaseError # Third party imports from celery import shared_task @@ -22,8 +23,12 @@ def recent_visited_task(entity_name, entity_identifier, user_id, project_id, slu ).first() if recent_visited: - recent_visited.visited_at = timezone.now() - recent_visited.save(update_fields=["visited_at"]) + # Check if the database is available + try: + recent_visited.visited_at = timezone.now() + recent_visited.save(update_fields=["visited_at"]) + except DatabaseError: + pass else: recent_visited_count = UserRecentVisit.objects.filter( user_id=user_id, workspace_id=workspace.id diff --git a/apiserver/plane/space/views/meta.py b/apiserver/plane/space/views/meta.py index fa441359964..d092e7e58eb 100644 --- a/apiserver/plane/space/views/meta.py +++ b/apiserver/plane/space/views/meta.py @@ -14,9 +14,9 @@ class ProjectMetaDataEndpoint(BaseAPIView): def get(self, request, anchor): try: - deploy_board = DeployBoard.objects.filter( + deploy_board = DeployBoard.objects.get( anchor=anchor, entity_name="project" - ).first() + ) except DeployBoard.DoesNotExist: return Response( {"error": "Project is not published"}, status=status.HTTP_404_NOT_FOUND