Skip to content

Commit

Permalink
Merge pull request RedHatInsights#1175 from coderbydesign/add-self-re…
Browse files Browse the repository at this point in the history
…ferencial-fk-on-workspace-model

Add self-referencial FK on Workspace model
  • Loading branch information
coderbydesign authored Sep 6, 2024
2 parents 6e3ed83 + c3fb8e9 commit d8051fd
Show file tree
Hide file tree
Showing 9 changed files with 178 additions and 39 deletions.
2 changes: 1 addition & 1 deletion docs/source/specs/typespec/main.tsp
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ namespace Workspaces {

model Workspace {
@key uuid: UUID;
parent?: UUID;
parent_id?: UUID;
...BasicWorkspace;
...Timestamps;
}
Expand Down
2 changes: 1 addition & 1 deletion docs/source/specs/v2/openapi.v2.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -666,7 +666,7 @@ components:
properties:
uuid:
$ref: '#/components/schemas/UUID'
parent:
parent_id:
$ref: '#/components/schemas/UUID'
name:
type: string
Expand Down
26 changes: 26 additions & 0 deletions rbac/management/migrations/0049_alter_workspace_parent.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Generated by Django 4.2.14 on 2024-08-29 19:42

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
("management", "0048_outbox"),
]

operations = [
migrations.AlterField(
model_name="workspace",
name="parent",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.PROTECT,
related_name="children",
to="management.workspace",
to_field="uuid",
),
),
]
4 changes: 3 additions & 1 deletion rbac/management/workspace/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@ class Workspace(TenantAwareModel):

name = models.CharField(max_length=255)
uuid = models.UUIDField(default=uuid4, editable=False, unique=True, null=False)
parent = models.UUIDField(null=True, blank=True, editable=True)
parent = models.ForeignKey(
"self", to_field="uuid", on_delete=models.PROTECT, related_name="children", null=True, blank=True
)
description = models.CharField(max_length=255, null=True, blank=True, editable=True)
created = models.DateTimeField(default=timezone.now)
modified = AutoDateTimeField(default=timezone.now)
Expand Down
11 changes: 4 additions & 7 deletions rbac/management/workspace/serializer.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,20 +27,17 @@ class WorkspaceSerializer(serializers.ModelSerializer):
name = serializers.CharField(required=False, max_length=255)
uuid = serializers.UUIDField(read_only=True, required=False)
description = serializers.CharField(allow_null=True, required=False, max_length=255)
parent = serializers.UUIDField(allow_null=True, required=False)
parent_id = serializers.UUIDField(allow_null=True, required=False)

class Meta:
"""Metadata for the serializer."""

model = Workspace
fields = ("name", "uuid", "parent", "description")
fields = ("name", "uuid", "parent_id", "description")

def create(self, validated_data):
"""Create the workspace object in the database."""
name = validated_data.pop("name")
description = validated_data.pop("description", "")
tenant = self.context["request"].tenant
parent = validated_data.pop("parent", None)
validated_data["tenant"] = self.context["request"].tenant

workspace = Workspace.objects.create(name=name, description=description, parent=parent, tenant=tenant)
workspace = Workspace.objects.create(**validated_data)
return workspace
25 changes: 13 additions & 12 deletions rbac/management/workspace/view.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@
from .model import Workspace
from .serializer import WorkspaceSerializer

VALID_PATCH_FIELDS = ["name", "description", "parent"]
REQUIRED_PUT_FIELDS = ["name", "description", "parent"]
VALID_PATCH_FIELDS = ["name", "description", "parent_id"]
REQUIRED_PUT_FIELDS = ["name", "description", "parent_id"]
REQUIRED_CREATE_FIELDS = ["name"]


Expand Down Expand Up @@ -66,7 +66,7 @@ def retrieve(self, request, *args, **kwargs):
def destroy(self, request, *args, **kwargs):
"""Delete a workspace."""
instance = self.get_object()
if Workspace.objects.filter(parent=instance.uuid, tenant=instance.tenant).exists():
if Workspace.objects.filter(parent=instance, tenant=instance.tenant).exists():
message = "Unable to delete due to workspace dependencies"
error = {"workspace": [_(message)]}
raise serializers.ValidationError(error)
Expand Down Expand Up @@ -94,9 +94,9 @@ def partial_update(self, request, *args, **kwargs):
def update_validation(self, request):
"""Validate a workspace for update."""
instance = self.get_object()
parent = request.data.get("parent")
if str(instance.uuid) == parent:
message = "Parent and UUID can't be same"
parent_id = request.data.get("parent_id")
if str(instance.uuid) == parent_id:
message = "Parent ID and UUID can't be same"
error = {"workspace": [_(message)]}
raise serializers.ValidationError(error)

Expand All @@ -110,18 +110,19 @@ def validate_required_fields(self, request, required_fields):

def validate_workspace(self, request, action="create"):
"""Validate a workspace."""
parent = request.data.get("parent")
parent_id = request.data.get("parent_id")
tenant = request.tenant
if action == "create":
self.validate_required_fields(request, REQUIRED_CREATE_FIELDS)
else:
self.validate_required_fields(request, REQUIRED_PUT_FIELDS)
if parent is None:
message = "Field 'parent' can't be null."
if parent_id is None:
message = "Field 'parent_id' can't be null."
error = {"workspace": [_(message)]}
raise serializers.ValidationError(error)
validate_uuid(parent)
if not Workspace.objects.filter(uuid=parent, tenant=tenant).exists():
message = f"Parent workspace '{parent}' doesn't exist in tenant"
if parent_id:
validate_uuid(parent_id)
if not Workspace.objects.filter(uuid=parent_id, tenant=tenant).exists():
message = f"Parent workspace '{parent_id}' doesn't exist in tenant"
error = {"workspace": [message]}
raise serializers.ValidationError(error)
47 changes: 47 additions & 0 deletions tests/management/workspace/test_model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
#
# Copyright 2024 Red Hat, Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
#
"""Test the workspace model."""
from management.models import Workspace
from tests.identity_request import IdentityRequest

from django.db.models import ProtectedError


class WorkspaceModelTests(IdentityRequest):
"""Test the workspace model."""

def setUp(self):
"""Set up the workspace model tests."""
super().setUp()

def tearDown(self):
"""Tear down workspace model tests."""
Workspace.objects.update(parent=None)
Workspace.objects.all().delete()

def test_child_parent_relations(self):
"""Test that workspaces can add/have parents as well as children"""
parent = Workspace.objects.create(name="Parent", tenant=self.tenant)
child = Workspace.objects.create(name="Child", tenant=self.tenant, parent=parent)
self.assertEqual(child.parent, parent)
self.assertEqual(list(parent.children.all()), [child])

def test_delete_fails_when_children(self):
"""Test that workspaces will not be deleted when children exist"""
parent = Workspace.objects.create(name="Parent", tenant=self.tenant)
child = Workspace.objects.create(name="Child", tenant=self.tenant, parent=parent)
self.assertRaises(ProtectedError, parent.delete)
65 changes: 65 additions & 0 deletions tests/management/workspace/test_serializer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
#
# Copyright 2024 Red Hat, Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
#
from django.test import TestCase
from unittest.mock import Mock
from api.models import Tenant
from management.models import Workspace
from management.workspace.serializer import WorkspaceSerializer
import uuid


class WorkspaceSerializerTest(TestCase):
"""Test the workspace serializer"""

def setUp(self):
"""Set up workspace serializer tests."""
tenant = Tenant.objects.get(tenant_name="public")
self.parent = Workspace.objects.create(
name="Parent", description="Parent desc", tenant=tenant, uuid=uuid.uuid4()
)
self.child = Workspace.objects.create(
name="Child", description="Child desc", tenant=tenant, parent=self.parent, uuid=uuid.uuid4()
)

def tearDown(self):
"""Tear down workspace serializer tests."""
Workspace.objects.update(parent=None)
Workspace.objects.all().delete()

def test_get_workspace_detail_child(self):
"""Return GET /workspace/<uuid>/ serializer response for child"""
serializer = WorkspaceSerializer(self.child)
expected_data = {
"uuid": str(self.child.uuid),
"name": self.child.name,
"description": self.child.description,
"parent_id": str(self.parent.uuid),
}

self.assertDictEqual(serializer.data, expected_data)

def test_get_workspace_detail_parent(self):
"""Return GET /workspace/<uuid>/ serializer response for parent"""
serializer = WorkspaceSerializer(self.parent)
expected_data = {
"uuid": str(self.parent.uuid),
"name": self.parent.name,
"description": self.parent.description,
"parent_id": None,
}

self.assertDictEqual(serializer.data, expected_data)
Loading

0 comments on commit d8051fd

Please sign in to comment.