diff --git a/apiserver/plane/db/migrations/0065_auto_20240415_0937.py b/apiserver/plane/db/migrations/0065_auto_20240415_0937.py index 9d8cc50be..57ede651a 100644 --- a/apiserver/plane/db/migrations/0065_auto_20240415_0937.py +++ b/apiserver/plane/db/migrations/0065_auto_20240415_0937.py @@ -257,4 +257,87 @@ class Migration(migrations.Migration): model_name="user", name="use_case", ), + migrations.CreateModel( + name="Site", + fields=[ + ( + "created_at", + models.DateTimeField( + auto_now_add=True, verbose_name="Created At" + ), + ), + ( + "updated_at", + models.DateTimeField( + auto_now=True, verbose_name="Last Modified At" + ), + ), + ( + "id", + models.UUIDField( + db_index=True, + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + unique=True, + ), + ), + ("name", models.CharField(max_length=80)), + ("description", models.TextField(blank=True)), + ("use_case", models.TextField(blank=True)), + ("domain", models.TextField(blank=True)), + ("user_count", models.IntegerField(default=1)), + ( + "created_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="%(class)s_created_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Created By", + ), + ), + ( + "owner", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="sites", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "updated_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="%(class)s_updated_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Last Modified By", + ), + ), + ], + options={ + "abstract": False, + "verbose_name": "Site", + "verbose_name_plural": "Sites", + "db_table": "sites", + "ordering": ("-created_at",), + }, + ), + migrations.AddField( + model_name="workspace", + name="site", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="workspaces", + to="db.site", + ), + ), + migrations.AddField( + model_name="workspace", + name="is_primary", + field=models.BooleanField(default=False), + ), ] diff --git a/apiserver/plane/db/models/__init__.py b/apiserver/plane/db/models/__init__.py index 2dc6d7909..39eb656b1 100644 --- a/apiserver/plane/db/models/__init__.py +++ b/apiserver/plane/db/models/__init__.py @@ -75,6 +75,7 @@ from .workspace import ( WorkspaceMemberInvite, WorkspaceTheme, WorkspaceUserProperties, + Site, ) from .importer import Importer diff --git a/apiserver/plane/db/models/workspace.py b/apiserver/plane/db/models/workspace.py index a64d59236..6a53e0e64 100644 --- a/apiserver/plane/db/models/workspace.py +++ b/apiserver/plane/db/models/workspace.py @@ -144,6 +144,12 @@ class Site(BaseModel): """Return name of site""" return self.name + class Meta: + verbose_name = "Site" + verbose_name_plural = "Sites" + db_table = "sites" + ordering = ("-created_at",) + class Workspace(BaseModel): name = models.CharField(max_length=80, verbose_name="Workspace Name") @@ -163,13 +169,24 @@ class Workspace(BaseModel): ) organization_size = models.CharField(max_length=20, blank=True, null=True) site = models.ForeignKey( - "db.Site", on_delete=models.CASCADE, related_name="workspaces" + "db.Site", + on_delete=models.CASCADE, + related_name="workspaces", + null=True, ) + is_primary = models.BooleanField(default=False) def __str__(self): """Return name of the Workspace""" return self.name + def save(self, *args, **kwargs): + if self.is_primary: + Workspace.objects.filter(site_id=self.site_id).update( + is_primary=False + ) + super(Workspace, self).save(*args, **kwargs) + class Meta: verbose_name = "Workspace" verbose_name_plural = "Workspaces" diff --git a/apiserver/plane/license/api/views/__init__.py b/apiserver/plane/license/api/views/__init__.py index cddaff0eb..e38e7cf72 100644 --- a/apiserver/plane/license/api/views/__init__.py +++ b/apiserver/plane/license/api/views/__init__.py @@ -16,4 +16,6 @@ from .admin import ( InstanceAdminSignUpEndpoint, InstanceAdminUserMeEndpoint, InstanceAdminSignOutEndpoint, + InstanceWorkspacesEndpoint, + PrimaryWorkspaceEndpoint, ) diff --git a/apiserver/plane/license/api/views/admin.py b/apiserver/plane/license/api/views/admin.py index c9c028f32..c9f3f72b8 100644 --- a/apiserver/plane/license/api/views/admin.py +++ b/apiserver/plane/license/api/views/admin.py @@ -25,7 +25,7 @@ from plane.license.api.serializers import ( InstanceAdminSerializer, ) from plane.license.models import Instance, InstanceAdmin -from plane.db.models import User, Profile +from plane.db.models import User, Profile, Workspace, Site from plane.utils.cache import cache_response, invalidate_cache from plane.authentication.utils.login import user_login from plane.authentication.utils.host import base_host @@ -392,3 +392,58 @@ class InstanceAdminSignOutEndpoint(View): "god-mode/login?" + urlencode({"success": "true"}), ) return HttpResponseRedirect(url) + + +class InstanceWorkspacesEndpoint(BaseAPIView): + permission_classes = [ + InstanceAdminPermission, + ] + + def get(self, request): + workspace = Workspace.objects.values() + return Response(workspace, status=status.HTTP_200_OK) + + +class PrimaryWorkspaceEndpoint(BaseAPIView): + + permission_classes = [ + InstanceAdminPermission, + ] + + def post(self, request): + # Get the id of the primary workspace + primary_workspace_id = request.data.get("primary_workspace_id", False) + + # Throw error is the primary workspace is not specified + if not primary_workspace_id: + return Response( + {"error": "Primary workspace id is required"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + # Get the primary workspace + primary_workspace = Workspace.objects.get(pk=primary_workspace_id) + + # Check if the site exists or not + site = Site.objects.first() + if not site: + # Create a Site + site = Site.objects.create( + name=primary_workspace.name, + owner=primary_workspace.owner, + domain=f"{request.get_host()}", + user_count=User.objects.count(), + ) + + # Attach this site to all workspaces + Workspace.objects.update(site=site, is_primary=False) + + # Update the primary workspace + primary_workspace.is_primary = True + primary_workspace.site = site + primary_workspace.save() + + return Response( + {"message": "Primary workspace created succesfully"}, + status=status.HTTP_200_OK, + ) diff --git a/apiserver/plane/license/api/views/instance.py b/apiserver/plane/license/api/views/instance.py index 40b3c7e0d..671f55be6 100644 --- a/apiserver/plane/license/api/views/instance.py +++ b/apiserver/plane/license/api/views/instance.py @@ -155,6 +155,9 @@ class InstanceEndpoint(BaseAPIView): ) instance_data = serializer.data instance_data["workspaces_exist"] = Workspace.objects.count() > 1 + instance_data["primary_workspace"] = Workspace.objects.filter( + is_primary=True + ).exists() response_data = {"config": data, "instance": instance_data} return Response(response_data, status=status.HTTP_200_OK) diff --git a/apiserver/plane/license/urls.py b/apiserver/plane/license/urls.py index b95ae74d6..92694767a 100644 --- a/apiserver/plane/license/urls.py +++ b/apiserver/plane/license/urls.py @@ -10,6 +10,8 @@ from plane.license.api.views import ( SignUpScreenVisitedEndpoint, InstanceAdminUserMeEndpoint, InstanceAdminSignOutEndpoint, + InstanceWorkspacesEndpoint, + PrimaryWorkspaceEndpoint, ) urlpatterns = [ @@ -63,4 +65,14 @@ urlpatterns = [ EmailCredentialCheckEndpoint.as_view(), name="email-credential-check", ), + path( + "workspaces/", + InstanceWorkspacesEndpoint.as_view(), + name="instance-workspaces", + ), + path( + "primary-workspace/", + PrimaryWorkspaceEndpoint.as_view(), + name="primary-workspace", + ), ]