fix: workspace and project uniqueness

This commit is contained in:
pablohashescobar 2022-12-13 23:42:41 +05:30
parent eb85e04a09
commit 36817e616b
3 changed files with 34 additions and 14 deletions

View File

@ -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):

View File

@ -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,

View File

@ -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"