Skip to content

Commit

Permalink
Merge branch 'master' into read-only-mode
Browse files Browse the repository at this point in the history
  • Loading branch information
coderbydesign authored Oct 3, 2024
2 parents 623c101 + 7c662c2 commit fe044c4
Show file tree
Hide file tree
Showing 9 changed files with 265 additions and 13 deletions.
15 changes: 15 additions & 0 deletions docs/source/specs/typespec/main.tsp
Original file line number Diff line number Diff line change
Expand Up @@ -96,9 +96,21 @@ namespace Workspaces {
description?: string = "Description of Workspace A";
}

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

enum WorkspaceTypesQueryParam {
"all",
...WorkspaceTypes
}

model Workspace {
@key uuid: UUID;
parent_id?: UUID;
type: WorkspaceTypes;
...BasicWorkspace;
...Timestamps;
}
Expand Down Expand Up @@ -208,6 +220,9 @@ namespace Workspaces {
@get op list(
@query limit?: int64 = 10;
@query offset?: int64 = 0;

@doc("Defaults to all when param is not supplied.")
@query type?: WorkspaceTypesQueryParam = WorkspaceTypesQueryParam.all;
): WorkspaceListResponse | Problems.CommonProblems;

@doc("Create workspace in tenant")
Expand Down
23 changes: 23 additions & 0 deletions docs/source/specs/v2/openapi.v2.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,13 @@ paths:
type: integer
format: int64
default: 0
- name: type
in: query
required: false
description: Defaults to all when param is not supplied.
schema:
$ref: '#/components/schemas/Workspaces.WorkspaceTypesQueryParam'
default: all
responses:
'200':
description: The request has succeeded.
Expand Down Expand Up @@ -765,6 +772,7 @@ components:
type: object
required:
- uuid
- type
- name
- created
- modified
Expand All @@ -773,6 +781,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 +831,19 @@ components:
items:
$ref: '#/components/schemas/Workspaces.Workspace'
description: List of workspaces
Workspaces.WorkspaceTypes:
type: string
enum:
- root
- default
- standard
Workspaces.WorkspaceTypesQueryParam:
type: string
enum:
- all
- 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
14 changes: 14 additions & 0 deletions rbac/management/workspace/view.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,20 @@ def retrieve(self, request, *args, **kwargs):
"""Get a workspace."""
return super().retrieve(request=request, args=args, kwargs=kwargs)

def list(self, request, *args, **kwargs):
"""Get a list of workspaces."""
all_types = "all"
queryset = self.get_queryset()
type_values = Workspace.Types.values + [all_types]
type_field = validate_and_get_key(request.query_params, "type", type_values, all_types)

if type_field != all_types:
queryset = queryset.filter(type=type_field)

serializer = self.get_serializer(queryset, many=True)
page = self.paginate_queryset(serializer.data)
return self.get_paginated_response(page)

def destroy(self, request, *args, **kwargs):
"""Delete a workspace."""
instance = self.get_object()
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
Loading

0 comments on commit fe044c4

Please sign in to comment.