diff --git a/apiserver/plane/api/serializers/project.py b/apiserver/plane/api/serializers/project.py index 34afca72b..cdc9adf36 100644 --- a/apiserver/plane/api/serializers/project.py +++ b/apiserver/plane/api/serializers/project.py @@ -29,12 +29,18 @@ class ProjectSerializer(BaseSerializer): if identifier == "": raise serializers.ValidationError(detail="Project Identifier is required") - if ProjectIdentifier.objects.filter(name=identifier).exists(): + if ProjectIdentifier.objects.filter( + name=identifier, workspace_id=self.context["workspace_id"] + ).exists(): raise serializers.ValidationError(detail="Project Identifier is taken") project = Project.objects.create( **validated_data, workspace_id=self.context["workspace_id"] ) - _ = ProjectIdentifier.objects.create(name=project.identifier, project=project) + _ = ProjectIdentifier.objects.create( + name=project.identifier, + project=project, + workspace_id=self.context["workspace_id"], + ) return project def update(self, instance, validated_data): @@ -47,7 +53,9 @@ class ProjectSerializer(BaseSerializer): return project # If no Project Identifier is found create it - project_identifier = ProjectIdentifier.objects.filter(name=identifier).first() + project_identifier = ProjectIdentifier.objects.filter( + name=identifier, workspace_id=instance.workspace_id + ).first() if project_identifier is None: project = super().update(instance, validated_data) @@ -61,9 +69,7 @@ class ProjectSerializer(BaseSerializer): return project # If not same fail update - raise serializers.ValidationError( - detail="Project Identifier is already taken" - ) + raise serializers.ValidationError(detail="Project Identifier is already taken") class ProjectDetailSerializer(BaseSerializer): diff --git a/apiserver/plane/api/views/project.py b/apiserver/plane/api/views/project.py index 1dc00e404..0685cebe4 100644 --- a/apiserver/plane/api/views/project.py +++ b/apiserver/plane/api/views/project.py @@ -144,9 +144,13 @@ class ProjectViewSet(BaseViewSet): except IntegrityError as e: if "already exists" in str(e): return Response( - {"name": "The project name is already taken"}, + {"identifier": "The project identifier is already taken"}, status=status.HTTP_410_GONE, ) + except Workspace.DoesNotExist as e: + return Response( + {"error": "Workspace does not exist"}, status=status.HTTP_404_NOT_FOUND + ) except serializers.ValidationError as e: return Response( {"identifier": "The project identifier is already taken"}, @@ -183,6 +187,10 @@ class ProjectViewSet(BaseViewSet): {"name": "The project name is already taken"}, status=status.HTTP_410_GONE, ) + except (Project.DoesNotExist or Workspace.DoesNotExist) as e: + return Response( + {"error": "Project does not exist"}, status=status.HTTP_404_NOT_FOUND + ) except serializers.ValidationError as e: return Response( {"identifier": "The project identifier is already taken"}, @@ -498,9 +506,9 @@ class ProjectIdentifierEndpoint(BaseAPIView): {"error": "Name is required"}, status=status.HTTP_400_BAD_REQUEST ) - exists = ProjectIdentifier.objects.filter(name=name).values( - "id", "name", "project" - ) + exists = ProjectIdentifier.objects.filter( + name=name, workspace__slug=slug + ).values("id", "name", "project") return Response( {"exists": len(exists), "identifiers": exists}, @@ -523,13 +531,13 @@ class ProjectIdentifierEndpoint(BaseAPIView): {"error": "Name is required"}, status=status.HTTP_400_BAD_REQUEST ) - if Project.objects.filter(identifier=name).exists(): + if Project.objects.filter(identifier=name, workspace__slug=slug).exists(): return Response( {"error": "Cannot delete an identifier of an existing project"}, status=status.HTTP_400_BAD_REQUEST, ) - ProjectIdentifier.objects.filter(name=name).delete() + ProjectIdentifier.objects.filter(name=name, workspace__slug=slug).delete() return Response( status=status.HTTP_204_NO_CONTENT, diff --git a/apiserver/plane/db/models/project.py b/apiserver/plane/db/models/project.py index 2e253ea1b..02fea2c7c 100644 --- a/apiserver/plane/db/models/project.py +++ b/apiserver/plane/db/models/project.py @@ -35,7 +35,8 @@ class Project(BaseModel): "db.WorkSpace", on_delete=models.CASCADE, related_name="workspace_project" ) identifier = models.CharField( - max_length=5, verbose_name="Project Identifier", null=True, blank=True + max_length=5, + verbose_name="Project Identifier", ) slug = models.SlugField(max_length=100, blank=True) default_assignee = models.ForeignKey( @@ -58,7 +59,7 @@ class Project(BaseModel): return f"{self.name} <{self.workspace.name}>" class Meta: - unique_together = ["name", "workspace"] + unique_together = ["identifier", "workspace"] verbose_name = "Project" verbose_name_plural = "Projects" db_table = "project" @@ -131,12 +132,17 @@ class ProjectMember(ProjectBaseModel): class ProjectIdentifier(AuditModel): + + workspace = models.ForeignKey( + "db.Workspace", models.CASCADE, related_name="project_identifiers", null=True + ) project = models.OneToOneField( Project, on_delete=models.CASCADE, related_name="project_identifier" ) name = models.CharField(max_length=10) class Meta: + unique_together = ["name", "workspace"] verbose_name = "Project Identifier" verbose_name_plural = "Project Identifiers" db_table = "project_identifier"