Skip to content

Commit

Permalink
Merge pull request RedHatInsights#1214 from coderbydesign/workspace-t…
Browse files Browse the repository at this point in the history
…ypes

Workspace types
  • Loading branch information
coderbydesign authored Oct 3, 2024
2 parents d460883 + 9f4278d commit 05ef5bb
Show file tree
Hide file tree
Showing 8 changed files with 154 additions and 4 deletions.
7 changes: 7 additions & 0 deletions docs/source/specs/typespec/main.tsp
Original file line number Diff line number Diff line change
Expand Up @@ -96,9 +96,16 @@ namespace Workspaces {
description?: string = "Description of Workspace A";
}

enum WorkspaceTypes {
"root",
"default",
"standard"
}

model Workspace {
@key uuid: UUID;
parent_id?: UUID;
type: WorkspaceTypes;
...BasicWorkspace;
...Timestamps;
}
Expand Down
9 changes: 9 additions & 0 deletions docs/source/specs/v2/openapi.v2.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -765,6 +765,7 @@ components:
type: object
required:
- uuid
- type
- name
- created
- modified
Expand All @@ -773,6 +774,8 @@ components:
$ref: '#/components/schemas/UUID'
parent_id:
$ref: '#/components/schemas/UUID'
type:
$ref: '#/components/schemas/Workspaces.WorkspaceTypes'
name:
type: string
description: Workspace A
Expand Down Expand Up @@ -821,6 +824,12 @@ components:
items:
$ref: '#/components/schemas/Workspaces.Workspace'
description: List of workspaces
Workspaces.WorkspaceTypes:
type: string
enum:
- root
- default
- standard
servers:
- url: https://console.redhat.com/{basePath}
description: Production Server
Expand Down
33 changes: 33 additions & 0 deletions rbac/management/migrations/0052_workspace_type_and_more.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Generated by Django 4.2.15 on 2024-10-03 12:54

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("management", "0051_alter_principal_user_id"),
]

operations = [
migrations.AddField(
model_name="workspace",
name="type",
field=models.CharField(
choices=[
("standard", "Standard"),
("default", "Default"),
("root", "Root"),
],
default="standard",
),
),
migrations.AddConstraint(
model_name="workspace",
constraint=models.UniqueConstraint(
condition=models.Q(("type__in", ["root", "default"])),
fields=("tenant_id", "type"),
name="unique_default_root_workspace_per_tenant",
),
),
]
19 changes: 19 additions & 0 deletions rbac/management/workspace/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from uuid import uuid4

from django.db import models
from django.db.models import Q, UniqueConstraint
from django.utils import timezone
from management.rbac_fields import AutoDateTimeField

Expand All @@ -27,17 +28,35 @@
class Workspace(TenantAwareModel):
"""A workspace."""

class Types(models.TextChoices):
STANDARD = "standard"
DEFAULT = "default"
ROOT = "root"

name = models.CharField(max_length=255)
uuid = models.UUIDField(default=uuid4, editable=False, unique=True, null=False)
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)
type = models.CharField(choices=Types.choices, default=Types.STANDARD, null=False)
created = models.DateTimeField(default=timezone.now)
modified = AutoDateTimeField(default=timezone.now)

class Meta:
ordering = ["name", "modified"]
constraints = [
UniqueConstraint(
fields=["tenant_id", "type"],
name="unique_default_root_workspace_per_tenant",
condition=Q(type__in=["root", "default"]),
)
]

def save(self, *args, **kwargs):
"""Override save on model to enforce validations."""
self.full_clean()
super().save(*args, **kwargs)

def ancestors(self):
"""Return a list of ancestors for a Workspace instance."""
Expand Down
2 changes: 2 additions & 0 deletions rbac/management/workspace/serializer.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ class WorkspaceSerializer(serializers.ModelSerializer):
parent_id = serializers.UUIDField(allow_null=True, required=False)
created = serializers.DateTimeField(read_only=True)
modified = serializers.DateTimeField(read_only=True)
type = serializers.CharField(read_only=True)

class Meta:
"""Metadata for the serializer."""
Expand All @@ -42,6 +43,7 @@ class Meta:
"description",
"created",
"modified",
"type",
)

def create(self, validated_data):
Expand Down
72 changes: 71 additions & 1 deletion tests/management/workspace/test_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,11 @@
# along with this program. If not, see <https://www.gnu.org/licenses/>.
#
"""Test the workspace model."""
from api.models import Tenant
from management.models import Workspace
from tests.identity_request import IdentityRequest

from django.core.exceptions import ValidationError
from django.db.models import ProtectedError


Expand Down Expand Up @@ -47,10 +49,78 @@ def test_delete_fails_when_children(self):
self.assertRaises(ProtectedError, parent.delete)

def test_ancestors(self):
"""Test ancestors on a workspce"""
"""Test ancestors on a workspace"""
root = Workspace.objects.create(name="Root", tenant=self.tenant, parent=None)
level_1 = Workspace.objects.create(name="Level 1", tenant=self.tenant, parent=root)
level_2 = Workspace.objects.create(name="Level 2", tenant=self.tenant, parent=level_1)
level_3 = Workspace.objects.create(name="Level 3", tenant=self.tenant, parent=level_2)
level_4 = Workspace.objects.create(name="Level 4", tenant=self.tenant, parent=level_3)
self.assertCountEqual(level_3.ancestors(), [root, level_1, level_2])


class Types(WorkspaceModelTests):
"""Test types on a workspace."""

def setUp(self):
"""Set up the workspace model tests."""
self.tenant_2 = Tenant.objects.create(tenant_name="foo")

self.tenant_1_root_workspace = Workspace.objects.create(
name="T1 Root Workspace", tenant=self.tenant, type="root"
)
self.tenant_1_default_workspace = Workspace.objects.create(
name="T1 Default Workspace", tenant=self.tenant, type="default"
)
self.tenant_1_standard_workspace = Workspace.objects.create(name="T1 Standard Workspace", tenant=self.tenant)
super().setUp()

def test_default_value(self):
"""Test the default value of a workspace type when not supplied"""
self.assertEqual(self.tenant_1_standard_workspace.type, "standard")

def test_single_root_per_tenant(self):
"""Test tenant can only have one root workspace"""
with self.assertRaises(ValidationError) as assertion:
Workspace.objects.create(name="T1 Root Workspace Number 2", type="root", tenant=self.tenant)
error_messages = assertion.exception.messages
self.assertEqual(len(error_messages), 1)
self.assertIn("unique_default_root_workspace_per_tenant", error_messages[0])

def test_single_default_per_tenant(self):
"""Test tenant can only have one default workspace"""
with self.assertRaises(ValidationError) as assertion:
Workspace.objects.create(name="T1 Default Workspace Number 2", type="default", tenant=self.tenant)
error_messages = assertion.exception.messages
self.assertEqual(len(error_messages), 1)
self.assertIn("unique_default_root_workspace_per_tenant", error_messages[0])

def test_multiple_root_multiple_tenants(self):
"""Test that multiple tenants can have more than one root workspace"""
try:
Workspace.objects.create(name="T2 Root Workspace Number 2", type="root", tenant=self.tenant_2)
except ValidationError as e:
self.fail("test_multiple_root_multiple_tenants raised ValidationError unexpectedly")

def test_multiple_default_multiple_tenants(self):
"""Test that multiple tenants can have more than one default workspace"""
try:
Workspace.objects.create(name="T2 Default Workspace Number 2", type="default", tenant=self.tenant_2)
except ValidationError as e:
self.fail("test_multiple_default_multiple_tenants raised ValidationError unexpectedly")

def test_multiple_standard_per_tenant(self):
"""Test tenant can have multiple standard workspaces"""
try:
for n in ["1", "2", "3"]:
Workspace.objects.create(name=f"T1 Standard Workspace Number {n}", type="standard", tenant=self.tenant)
except ValidationError as e:
self.fail("test_multiple_standard_per_tenant raised ValidationError unexpectedly")

def test_invalid_type(self):
"""Test invalid workspace type"""
invalid_type = "foo"
with self.assertRaises(ValidationError) as assertion:
Workspace.objects.create(name="Invalid Type Workspace", type=invalid_type, tenant=self.tenant)
error_messages = assertion.exception.messages
self.assertEqual(len(error_messages), 1)
self.assertEqual(f"Value '{invalid_type}' is not a valid choice.", error_messages[0])
3 changes: 3 additions & 0 deletions tests/management/workspace/test_serializer.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ def test_get_workspace_detail_child(self):
"parent_id": str(self.parent.uuid),
"created": self._format_timestamps(self.child.created),
"modified": self._format_timestamps(self.child.modified),
"type": self.child.type,
}

self.assertDictEqual(serializer.data, expected_data)
Expand All @@ -71,6 +72,7 @@ def test_get_workspace_detail_parent(self):
"parent_id": None,
"created": self._format_timestamps(self.parent.created),
"modified": self._format_timestamps(self.parent.modified),
"type": self.parent.type,
}

self.assertDictEqual(serializer.data, expected_data)
Expand All @@ -88,6 +90,7 @@ def test_get_workspace_detail_with_ancestry(self):
"ancestry": [{"name": self.parent.name, "uuid": str(self.parent.uuid), "parent_id": None}],
"created": self._format_timestamps(self.child.created),
"modified": self._format_timestamps(self.child.modified),
"type": self.child.type,
}

self.assertDictEqual(serializer.data, expected_data)
Expand Down
13 changes: 10 additions & 3 deletions tests/management/workspace/test_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ def test_create_workspace(self):
"name": "New Workspace",
"description": "New Workspace - description",
"tenant_id": self.tenant.id,
"parent_id": "cbe9822d-cadb-447d-bc80-8bef773c36ea",
"parent_id": self.init_workspace.uuid,
}

parent_workspace = Workspace.objects.create(**workspace_data)
Expand All @@ -79,6 +79,7 @@ def test_create_workspace(self):
self.assertNotEquals(data.get("created"), "")
self.assertNotEquals(data.get("modified"), "")
self.assertEquals(data.get("description"), "Workspace")
self.assertEquals(data.get("type"), "standard")
self.assertEqual(response.get("content-type"), "application/json")

def test_create_workspace_without_parent(self):
Expand All @@ -96,6 +97,7 @@ def test_create_workspace_without_parent(self):
self.assertNotEquals(data.get("created"), "")
self.assertNotEquals(data.get("modified"), "")
self.assertEquals(data.get("description"), "Workspace")
self.assertEquals(data.get("type"), "standard")
self.assertEqual(response.get("content-type"), "application/json")

def test_create_workspace_empty_body(self):
Expand Down Expand Up @@ -179,6 +181,7 @@ def test_update_workspace(self):
self.assertIsNotNone(data.get("uuid"))
self.assertNotEquals(data.get("created"), "")
self.assertNotEquals(data.get("modified"), "")
self.assertEquals(data.get("type"), "standard")
self.assertEquals(data.get("description"), "Updated description")

update_workspace = Workspace.objects.filter(id=workspace.id).first()
Expand All @@ -192,7 +195,7 @@ def test_partial_update_workspace_with_put_method(self):
"name": "New Workspace",
"description": "New Workspace - description",
"tenant_id": self.tenant.id,
"parent_id": "cbe9822d-cadb-447d-bc80-8bef773c36ea",
"parent_id": self.init_workspace.uuid,
}

workspace = Workspace.objects.create(**workspace_data)
Expand Down Expand Up @@ -220,7 +223,7 @@ def test_update_workspace_same_parent(self):
"name": "New Workspace",
"description": "New Workspace - description",
"tenant_id": self.tenant.id,
"parent_id": "cbe9822d-cadb-447d-bc80-8bef773c36ea",
"parent_id": self.init_workspace.uuid,
}

parent_workspace = Workspace.objects.create(**parent_workspace_data)
Expand Down Expand Up @@ -305,6 +308,7 @@ def test_partial_update_workspace(self):
self.assertIsNotNone(data.get("uuid"))
self.assertNotEquals(data.get("created"), "")
self.assertNotEquals(data.get("modified"), "")
self.assertEquals(data.get("type"), "standard")

update_workspace = Workspace.objects.filter(id=workspace.id).first()
self.assertEquals(update_workspace.name, "Updated name")
Expand Down Expand Up @@ -395,6 +399,7 @@ def test_get_workspace(self):
self.assertNotEquals(data.get("modified"), "")
self.assertEqual(response.get("content-type"), "application/json")
self.assertEqual(data.get("ancestry"), None)
self.assertEquals(data.get("type"), "standard")
self.assertEqual(response.get("content-type"), "application/json")

def test_get_workspace_with_ancestry(self):
Expand All @@ -415,6 +420,7 @@ def test_get_workspace_with_ancestry(self):
data.get("ancestry"),
[{"name": self.parent_workspace.name, "uuid": str(self.parent_workspace.uuid), "parent_id": None}],
)
self.assertEquals(data.get("type"), "standard")
self.assertEqual(response.get("content-type"), "application/json")
self.assertEqual(data.get("ancestry"), None)

Expand All @@ -436,6 +442,7 @@ def test_get_workspace_with_ancestry(self):
data.get("ancestry"),
[{"name": self.parent_workspace.name, "uuid": str(self.parent_workspace.uuid), "parent_id": None}],
)
self.assertEquals(data.get("type"), "standard")
self.assertEqual(response.get("content-type"), "application/json")

def test_get_workspace_not_found(self):
Expand Down

0 comments on commit 05ef5bb

Please sign in to comment.