feat: workspace management from admin app (#6093)
* feat: workspace management from admin app * chore: UI and UX copy improvements * chore: ux copy improvements
This commit is contained in:
parent
9dbb2b26c3
commit
05d3e3ae45
53 changed files with 1153 additions and 122 deletions
|
|
@ -1,6 +1,7 @@
|
|||
# Python imports
|
||||
import csv
|
||||
import io
|
||||
import os
|
||||
from datetime import date
|
||||
|
||||
from dateutil.relativedelta import relativedelta
|
||||
|
|
@ -38,7 +39,7 @@ from django.utils.decorators import method_decorator
|
|||
from django.views.decorators.cache import cache_control
|
||||
from django.views.decorators.vary import vary_on_cookie
|
||||
from plane.utils.constants import RESTRICTED_WORKSPACE_SLUGS
|
||||
|
||||
from plane.license.utils.instance_value import get_configuration_value
|
||||
|
||||
class WorkSpaceViewSet(BaseViewSet):
|
||||
model = Workspace
|
||||
|
|
@ -80,6 +81,21 @@ class WorkSpaceViewSet(BaseViewSet):
|
|||
|
||||
def create(self, request):
|
||||
try:
|
||||
DISABLE_WORKSPACE_CREATION, = get_configuration_value(
|
||||
[
|
||||
{
|
||||
"key": "DISABLE_WORKSPACE_CREATION",
|
||||
"default": os.environ.get("DISABLE_WORKSPACE_CREATION", "0"),
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
if DISABLE_WORKSPACE_CREATION == "1":
|
||||
return Response(
|
||||
{"error": "Workspace creation is not allowed"},
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
|
||||
serializer = WorkSpaceSerializer(data=request.data)
|
||||
|
||||
slug = request.data.get("slug", False)
|
||||
|
|
|
|||
|
|
@ -2,3 +2,4 @@ from .instance import InstanceSerializer
|
|||
|
||||
from .configuration import InstanceConfigurationSerializer
|
||||
from .admin import InstanceAdminSerializer, InstanceAdminMeSerializer
|
||||
from .workspace import WorkspaceSerializer
|
||||
6
apiserver/plane/license/api/serializers/user.py
Normal file
6
apiserver/plane/license/api/serializers/user.py
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
from .base import BaseSerializer
|
||||
from plane.db.models import User
|
||||
class UserLiteSerializer(BaseSerializer):
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ["id", "email", "first_name", "last_name",]
|
||||
34
apiserver/plane/license/api/serializers/workspace.py
Normal file
34
apiserver/plane/license/api/serializers/workspace.py
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
# Third Party Imports
|
||||
from rest_framework import serializers
|
||||
|
||||
# Module imports
|
||||
from .base import BaseSerializer
|
||||
from .user import UserLiteSerializer
|
||||
from plane.db.models import Workspace
|
||||
from plane.utils.constants import RESTRICTED_WORKSPACE_SLUGS
|
||||
|
||||
|
||||
class WorkspaceSerializer(BaseSerializer):
|
||||
owner = UserLiteSerializer(read_only=True)
|
||||
logo_url = serializers.CharField(read_only=True)
|
||||
total_projects = serializers.IntegerField(read_only=True)
|
||||
total_members = serializers.IntegerField(read_only=True)
|
||||
|
||||
def validate_slug(self, value):
|
||||
# Check if the slug is restricted
|
||||
if value in RESTRICTED_WORKSPACE_SLUGS:
|
||||
raise serializers.ValidationError("Slug is not valid")
|
||||
return value
|
||||
|
||||
class Meta:
|
||||
model = Workspace
|
||||
fields = "__all__"
|
||||
read_only_fields = [
|
||||
"id",
|
||||
"created_by",
|
||||
"updated_by",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"owner",
|
||||
"logo_url",
|
||||
]
|
||||
|
|
@ -14,3 +14,5 @@ from .admin import (
|
|||
)
|
||||
|
||||
from .changelog import ChangeLogEndpoint
|
||||
|
||||
from .workspace import InstanceWorkSpaceAvailabilityCheckEndpoint, InstanceWorkSpaceEndpoint
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ class InstanceEndpoint(BaseAPIView):
|
|||
# Get all the configuration
|
||||
(
|
||||
ENABLE_SIGNUP,
|
||||
DISABLE_WORKSPACE_CREATION,
|
||||
IS_GOOGLE_ENABLED,
|
||||
IS_GITHUB_ENABLED,
|
||||
GITHUB_APP_NAME,
|
||||
|
|
@ -65,6 +66,10 @@ class InstanceEndpoint(BaseAPIView):
|
|||
"key": "ENABLE_SIGNUP",
|
||||
"default": os.environ.get("ENABLE_SIGNUP", "0"),
|
||||
},
|
||||
{
|
||||
"key": "DISABLE_WORKSPACE_CREATION",
|
||||
"default": os.environ.get("DISABLE_WORKSPACE_CREATION", "0"),
|
||||
},
|
||||
{
|
||||
"key": "IS_GOOGLE_ENABLED",
|
||||
"default": os.environ.get("IS_GOOGLE_ENABLED", "0"),
|
||||
|
|
@ -125,6 +130,7 @@ class InstanceEndpoint(BaseAPIView):
|
|||
data = {}
|
||||
# Authentication
|
||||
data["enable_signup"] = ENABLE_SIGNUP == "1"
|
||||
data["is_workspace_creation_disabled"] = DISABLE_WORKSPACE_CREATION == "1"
|
||||
data["is_google_enabled"] = IS_GOOGLE_ENABLED == "1"
|
||||
data["is_github_enabled"] = IS_GITHUB_ENABLED == "1"
|
||||
data["is_gitlab_enabled"] = IS_GITLAB_ENABLED == "1"
|
||||
|
|
|
|||
115
apiserver/plane/license/api/views/workspace.py
Normal file
115
apiserver/plane/license/api/views/workspace.py
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
# Third party imports
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import status
|
||||
from django.db import IntegrityError
|
||||
from django.db.models import OuterRef, Func, F
|
||||
|
||||
# Module imports
|
||||
from plane.app.views.base import BaseAPIView
|
||||
from plane.license.api.permissions import InstanceAdminPermission
|
||||
from plane.db.models import Workspace, WorkspaceMember, Project
|
||||
from plane.license.api.serializers import WorkspaceSerializer
|
||||
from plane.utils.constants import RESTRICTED_WORKSPACE_SLUGS
|
||||
|
||||
|
||||
class InstanceWorkSpaceAvailabilityCheckEndpoint(BaseAPIView):
|
||||
permission_classes = [InstanceAdminPermission]
|
||||
|
||||
def get(self, request):
|
||||
slug = request.GET.get("slug", False)
|
||||
|
||||
if not slug or slug == "":
|
||||
return Response(
|
||||
{"error": "Workspace Slug is required"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
workspace = (
|
||||
Workspace.objects.filter(slug=slug).exists()
|
||||
or slug in RESTRICTED_WORKSPACE_SLUGS
|
||||
)
|
||||
return Response({"status": not workspace}, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
class InstanceWorkSpaceEndpoint(BaseAPIView):
|
||||
model = Workspace
|
||||
serializer_class = WorkspaceSerializer
|
||||
permission_classes = [InstanceAdminPermission]
|
||||
|
||||
def get(self, request):
|
||||
project_count = (
|
||||
Project.objects.filter(workspace_id=OuterRef("id"))
|
||||
.order_by()
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
.values("count")
|
||||
)
|
||||
|
||||
member_count = (
|
||||
WorkspaceMember.objects.filter(
|
||||
workspace=OuterRef("id"), member__is_bot=False, is_active=True
|
||||
).select_related("owner")
|
||||
.order_by()
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
.values("count")
|
||||
)
|
||||
|
||||
workspaces = Workspace.objects.annotate(
|
||||
total_projects=project_count,
|
||||
total_members=member_count,
|
||||
)
|
||||
|
||||
# Add search functionality
|
||||
search = request.query_params.get("search", None)
|
||||
if search:
|
||||
workspaces = workspaces.filter(name__icontains=search)
|
||||
|
||||
return self.paginate(
|
||||
request=request,
|
||||
queryset=workspaces,
|
||||
on_results=lambda results: WorkspaceSerializer(
|
||||
results, many=True,
|
||||
).data,
|
||||
max_per_page=10,
|
||||
default_per_page=10,
|
||||
)
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
serializer = WorkspaceSerializer (data=request.data)
|
||||
|
||||
slug = request.data.get("slug", False)
|
||||
name = request.data.get("name", False)
|
||||
|
||||
if not name or not slug:
|
||||
return Response(
|
||||
{"error": "Both name and slug are required"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
if len(name) > 80 or len(slug) > 48:
|
||||
return Response(
|
||||
{"error": "The maximum length for name is 80 and for slug is 48"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
if serializer.is_valid(raise_exception=True):
|
||||
serializer.save(owner=request.user)
|
||||
# Create Workspace member
|
||||
_ = WorkspaceMember.objects.create(
|
||||
workspace_id=serializer.data["id"],
|
||||
member=request.user,
|
||||
role=20,
|
||||
company_role=request.data.get("company_role", ""),
|
||||
)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
return Response(
|
||||
[serializer.errors[error][0] for error in serializer.errors],
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
except IntegrityError as e:
|
||||
if "already exists" in str(e):
|
||||
return Response(
|
||||
{"slug": "The workspace with the slug already exists"},
|
||||
status=status.HTTP_410_GONE,
|
||||
)
|
||||
|
|
@ -29,6 +29,12 @@ class Command(BaseCommand):
|
|||
"category": "AUTHENTICATION",
|
||||
"is_encrypted": False,
|
||||
},
|
||||
{
|
||||
"key": "DISABLE_WORKSPACE_CREATION",
|
||||
"value": os.environ.get("DISABLE_WORKSPACE_CREATION", "0"),
|
||||
"category": "WORKSPACE_MANAGEMENT",
|
||||
"is_encrypted": False,
|
||||
},
|
||||
{
|
||||
"key": "ENABLE_EMAIL_PASSWORD",
|
||||
"value": os.environ.get("ENABLE_EMAIL_PASSWORD", "1"),
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ from plane.license.api.views import (
|
|||
InstanceAdminSignOutEndpoint,
|
||||
InstanceAdminUserSessionEndpoint,
|
||||
ChangeLogEndpoint,
|
||||
InstanceWorkSpaceAvailabilityCheckEndpoint,
|
||||
InstanceWorkSpaceEndpoint,
|
||||
)
|
||||
|
||||
urlpatterns = [
|
||||
|
|
@ -55,4 +57,14 @@ urlpatterns = [
|
|||
EmailCredentialCheckEndpoint.as_view(),
|
||||
name="email-credential-check",
|
||||
),
|
||||
path(
|
||||
"workspace-slug-check/",
|
||||
InstanceWorkSpaceAvailabilityCheckEndpoint.as_view(),
|
||||
name="instance-workspace-availability",
|
||||
),
|
||||
path(
|
||||
"workspaces/",
|
||||
InstanceWorkSpaceEndpoint.as_view(),
|
||||
name="instance-workspace",
|
||||
),
|
||||
]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue