forked from github/plane
fix: workspace and project uniqueness
This commit is contained in:
parent
eb85e04a09
commit
36817e616b
@ -29,12 +29,18 @@ class ProjectSerializer(BaseSerializer):
|
|||||||
if identifier == "":
|
if identifier == "":
|
||||||
raise serializers.ValidationError(detail="Project Identifier is required")
|
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")
|
raise serializers.ValidationError(detail="Project Identifier is taken")
|
||||||
project = Project.objects.create(
|
project = Project.objects.create(
|
||||||
**validated_data, workspace_id=self.context["workspace_id"]
|
**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
|
return project
|
||||||
|
|
||||||
def update(self, instance, validated_data):
|
def update(self, instance, validated_data):
|
||||||
@ -47,7 +53,9 @@ class ProjectSerializer(BaseSerializer):
|
|||||||
return project
|
return project
|
||||||
|
|
||||||
# If no Project Identifier is found create it
|
# 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:
|
if project_identifier is None:
|
||||||
project = super().update(instance, validated_data)
|
project = super().update(instance, validated_data)
|
||||||
@ -61,9 +69,7 @@ class ProjectSerializer(BaseSerializer):
|
|||||||
return project
|
return project
|
||||||
|
|
||||||
# If not same fail update
|
# If not same fail update
|
||||||
raise serializers.ValidationError(
|
raise serializers.ValidationError(detail="Project Identifier is already taken")
|
||||||
detail="Project Identifier is already taken"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class ProjectDetailSerializer(BaseSerializer):
|
class ProjectDetailSerializer(BaseSerializer):
|
||||||
|
@ -144,9 +144,13 @@ class ProjectViewSet(BaseViewSet):
|
|||||||
except IntegrityError as e:
|
except IntegrityError as e:
|
||||||
if "already exists" in str(e):
|
if "already exists" in str(e):
|
||||||
return Response(
|
return Response(
|
||||||
{"name": "The project name is already taken"},
|
{"identifier": "The project identifier is already taken"},
|
||||||
status=status.HTTP_410_GONE,
|
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:
|
except serializers.ValidationError as e:
|
||||||
return Response(
|
return Response(
|
||||||
{"identifier": "The project identifier is already taken"},
|
{"identifier": "The project identifier is already taken"},
|
||||||
@ -183,6 +187,10 @@ class ProjectViewSet(BaseViewSet):
|
|||||||
{"name": "The project name is already taken"},
|
{"name": "The project name is already taken"},
|
||||||
status=status.HTTP_410_GONE,
|
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:
|
except serializers.ValidationError as e:
|
||||||
return Response(
|
return Response(
|
||||||
{"identifier": "The project identifier is already taken"},
|
{"identifier": "The project identifier is already taken"},
|
||||||
@ -498,9 +506,9 @@ class ProjectIdentifierEndpoint(BaseAPIView):
|
|||||||
{"error": "Name is required"}, status=status.HTTP_400_BAD_REQUEST
|
{"error": "Name is required"}, status=status.HTTP_400_BAD_REQUEST
|
||||||
)
|
)
|
||||||
|
|
||||||
exists = ProjectIdentifier.objects.filter(name=name).values(
|
exists = ProjectIdentifier.objects.filter(
|
||||||
"id", "name", "project"
|
name=name, workspace__slug=slug
|
||||||
)
|
).values("id", "name", "project")
|
||||||
|
|
||||||
return Response(
|
return Response(
|
||||||
{"exists": len(exists), "identifiers": exists},
|
{"exists": len(exists), "identifiers": exists},
|
||||||
@ -523,13 +531,13 @@ class ProjectIdentifierEndpoint(BaseAPIView):
|
|||||||
{"error": "Name is required"}, status=status.HTTP_400_BAD_REQUEST
|
{"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(
|
return Response(
|
||||||
{"error": "Cannot delete an identifier of an existing project"},
|
{"error": "Cannot delete an identifier of an existing project"},
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
ProjectIdentifier.objects.filter(name=name).delete()
|
ProjectIdentifier.objects.filter(name=name, workspace__slug=slug).delete()
|
||||||
|
|
||||||
return Response(
|
return Response(
|
||||||
status=status.HTTP_204_NO_CONTENT,
|
status=status.HTTP_204_NO_CONTENT,
|
||||||
|
@ -35,7 +35,8 @@ class Project(BaseModel):
|
|||||||
"db.WorkSpace", on_delete=models.CASCADE, related_name="workspace_project"
|
"db.WorkSpace", on_delete=models.CASCADE, related_name="workspace_project"
|
||||||
)
|
)
|
||||||
identifier = models.CharField(
|
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)
|
slug = models.SlugField(max_length=100, blank=True)
|
||||||
default_assignee = models.ForeignKey(
|
default_assignee = models.ForeignKey(
|
||||||
@ -58,7 +59,7 @@ class Project(BaseModel):
|
|||||||
return f"{self.name} <{self.workspace.name}>"
|
return f"{self.name} <{self.workspace.name}>"
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
unique_together = ["name", "workspace"]
|
unique_together = ["identifier", "workspace"]
|
||||||
verbose_name = "Project"
|
verbose_name = "Project"
|
||||||
verbose_name_plural = "Projects"
|
verbose_name_plural = "Projects"
|
||||||
db_table = "project"
|
db_table = "project"
|
||||||
@ -131,12 +132,17 @@ class ProjectMember(ProjectBaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class ProjectIdentifier(AuditModel):
|
class ProjectIdentifier(AuditModel):
|
||||||
|
|
||||||
|
workspace = models.ForeignKey(
|
||||||
|
"db.Workspace", models.CASCADE, related_name="project_identifiers", null=True
|
||||||
|
)
|
||||||
project = models.OneToOneField(
|
project = models.OneToOneField(
|
||||||
Project, on_delete=models.CASCADE, related_name="project_identifier"
|
Project, on_delete=models.CASCADE, related_name="project_identifier"
|
||||||
)
|
)
|
||||||
name = models.CharField(max_length=10)
|
name = models.CharField(max_length=10)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
unique_together = ["name", "workspace"]
|
||||||
verbose_name = "Project Identifier"
|
verbose_name = "Project Identifier"
|
||||||
verbose_name_plural = "Project Identifiers"
|
verbose_name_plural = "Project Identifiers"
|
||||||
db_table = "project_identifier"
|
db_table = "project_identifier"
|
||||||
|
Loading…
Reference in New Issue
Block a user