[WEB-5237] feat: add workspace invitation and project member management endpoints (#8059)
This commit is contained in:
parent
96bbbec588
commit
3c6f24de64
18 changed files with 982 additions and 17 deletions
|
|
@ -53,3 +53,5 @@ from .asset import (
|
|||
GenericAssetUpdateSerializer,
|
||||
FileAssetSerializer,
|
||||
)
|
||||
from .invite import WorkspaceInviteSerializer
|
||||
from .member import ProjectMemberSerializer
|
||||
56
apps/api/plane/api/serializers/invite.py
Normal file
56
apps/api/plane/api/serializers/invite.py
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
# Django imports
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.validators import validate_email
|
||||
from rest_framework import serializers
|
||||
|
||||
# Module imports
|
||||
from plane.db.models import WorkspaceMemberInvite
|
||||
from .base import BaseSerializer
|
||||
from plane.app.permissions.base import ROLE
|
||||
|
||||
|
||||
class WorkspaceInviteSerializer(BaseSerializer):
|
||||
"""
|
||||
Serializer for workspace invites.
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = WorkspaceMemberInvite
|
||||
fields = [
|
||||
"id",
|
||||
"email",
|
||||
"role",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"responded_at",
|
||||
"accepted",
|
||||
]
|
||||
read_only_fields = [
|
||||
"id",
|
||||
"workspace",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"responded_at",
|
||||
"accepted",
|
||||
]
|
||||
|
||||
def validate_email(self, value):
|
||||
try:
|
||||
validate_email(value)
|
||||
except ValidationError:
|
||||
raise serializers.ValidationError("Invalid email address", code="INVALID_EMAIL_ADDRESS")
|
||||
return value
|
||||
|
||||
def validate_role(self, value):
|
||||
if value not in [ROLE.ADMIN.value, ROLE.MEMBER.value, ROLE.GUEST.value]:
|
||||
raise serializers.ValidationError("Invalid role", code="INVALID_WORKSPACE_MEMBER_ROLE")
|
||||
return value
|
||||
|
||||
def validate(self, data):
|
||||
slug = self.context["slug"]
|
||||
if (
|
||||
data.get("email")
|
||||
and WorkspaceMemberInvite.objects.filter(email=data["email"], workspace__slug=slug).exists()
|
||||
):
|
||||
raise serializers.ValidationError("Email already invited", code="EMAIL_ALREADY_INVITED")
|
||||
return data
|
||||
39
apps/api/plane/api/serializers/member.py
Normal file
39
apps/api/plane/api/serializers/member.py
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
# Third party imports
|
||||
from rest_framework import serializers
|
||||
|
||||
# Module imports
|
||||
from plane.db.models import ProjectMember, WorkspaceMember
|
||||
from .base import BaseSerializer
|
||||
from plane.db.models import User
|
||||
from plane.utils.permissions import ROLE
|
||||
|
||||
|
||||
class ProjectMemberSerializer(BaseSerializer):
|
||||
"""
|
||||
Serializer for project members.
|
||||
"""
|
||||
|
||||
member = serializers.PrimaryKeyRelatedField(
|
||||
queryset=User.objects.all(),
|
||||
required=True,
|
||||
)
|
||||
|
||||
def validate_member(self, value):
|
||||
slug = self.context.get("slug")
|
||||
if not slug:
|
||||
raise serializers.ValidationError("Slug is required", code="INVALID_SLUG")
|
||||
if not value:
|
||||
raise serializers.ValidationError("Member is required", code="INVALID_MEMBER")
|
||||
if not WorkspaceMember.objects.filter(workspace__slug=slug, member=value).exists():
|
||||
raise serializers.ValidationError("Member not found in workspace", code="INVALID_MEMBER")
|
||||
return value
|
||||
|
||||
def validate_role(self, value):
|
||||
if value not in [ROLE.ADMIN.value, ROLE.MEMBER.value, ROLE.GUEST.value]:
|
||||
raise serializers.ValidationError("Invalid role", code="INVALID_ROLE")
|
||||
return value
|
||||
|
||||
class Meta:
|
||||
model = ProjectMember
|
||||
fields = ["id", "member", "role"]
|
||||
read_only_fields = ["id"]
|
||||
|
|
@ -8,6 +8,7 @@ from .project import urlpatterns as project_patterns
|
|||
from .state import urlpatterns as state_patterns
|
||||
from .user import urlpatterns as user_patterns
|
||||
from .work_item import urlpatterns as work_item_patterns
|
||||
from .invite import urlpatterns as invite_patterns
|
||||
|
||||
urlpatterns = [
|
||||
*asset_patterns,
|
||||
|
|
@ -20,4 +21,5 @@ urlpatterns = [
|
|||
*state_patterns,
|
||||
*user_patterns,
|
||||
*work_item_patterns,
|
||||
*invite_patterns,
|
||||
]
|
||||
|
|
|
|||
18
apps/api/plane/api/urls/invite.py
Normal file
18
apps/api/plane/api/urls/invite.py
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
# Django imports
|
||||
from django.urls import path, include
|
||||
|
||||
# Third party imports
|
||||
from rest_framework.routers import DefaultRouter
|
||||
|
||||
# Module imports
|
||||
from plane.api.views import WorkspaceInvitationsViewset
|
||||
|
||||
|
||||
# Create router with just the invitations prefix (no workspace slug)
|
||||
router = DefaultRouter()
|
||||
router.register(r"invitations", WorkspaceInvitationsViewset, basename="workspace-invitations")
|
||||
|
||||
# Wrap the router URLs with the workspace slug path
|
||||
urlpatterns = [
|
||||
path("workspaces/<str:slug>/", include(router.urls)),
|
||||
]
|
||||
|
|
@ -1,13 +1,29 @@
|
|||
from django.urls import path
|
||||
|
||||
from plane.api.views import ProjectMemberAPIEndpoint, WorkspaceMemberAPIEndpoint
|
||||
from plane.api.views import ProjectMemberListCreateAPIEndpoint, ProjectMemberDetailAPIEndpoint, WorkspaceMemberAPIEndpoint
|
||||
|
||||
urlpatterns = [
|
||||
# Project members
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/members/",
|
||||
ProjectMemberAPIEndpoint.as_view(http_method_names=["get"]),
|
||||
ProjectMemberListCreateAPIEndpoint.as_view(http_method_names=["get", "post"]),
|
||||
name="project-members",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/members/<uuid:pk>/",
|
||||
ProjectMemberDetailAPIEndpoint.as_view(http_method_names=["patch", "delete", "get"]),
|
||||
name="project-member",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/project-members/",
|
||||
ProjectMemberListCreateAPIEndpoint.as_view(http_method_names=["get", "post"]),
|
||||
name="project-members",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/project-members/<uuid:pk>/",
|
||||
ProjectMemberDetailAPIEndpoint.as_view(http_method_names=["patch", "delete", "get"]),
|
||||
name="project-member",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/members/",
|
||||
WorkspaceMemberAPIEndpoint.as_view(http_method_names=["get"]),
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ from .module import (
|
|||
ModuleArchiveUnarchiveAPIEndpoint,
|
||||
)
|
||||
|
||||
from .member import ProjectMemberAPIEndpoint, WorkspaceMemberAPIEndpoint
|
||||
from .member import ProjectMemberListCreateAPIEndpoint, ProjectMemberDetailAPIEndpoint, WorkspaceMemberAPIEndpoint
|
||||
|
||||
from .intake import (
|
||||
IntakeIssueListCreateAPIEndpoint,
|
||||
|
|
@ -53,3 +53,5 @@ from .intake import (
|
|||
from .asset import UserAssetEndpoint, UserServerAssetEndpoint, GenericAssetEndpoint
|
||||
|
||||
from .user import UserEndpoint
|
||||
|
||||
from .invite import WorkspaceInvitationsViewset
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
# Python imports
|
||||
import zoneinfo
|
||||
import logging
|
||||
|
||||
# Django imports
|
||||
from django.conf import settings
|
||||
|
|
@ -7,15 +8,19 @@ from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
|||
from django.db import IntegrityError
|
||||
from django.urls import resolve
|
||||
from django.utils import timezone
|
||||
from plane.db.models.api import APIToken
|
||||
|
||||
# Third party imports
|
||||
from rest_framework import status
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
|
||||
# Third party imports
|
||||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
from rest_framework.filters import SearchFilter
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
from rest_framework.exceptions import APIException
|
||||
from rest_framework.generics import GenericAPIView
|
||||
|
||||
# Module imports
|
||||
from plane.db.models.api import APIToken
|
||||
from plane.api.middleware.api_authentication import APIKeyAuthentication
|
||||
from plane.api.rate_limit import ApiKeyRateThrottle, ServiceTokenRateThrottle
|
||||
from plane.utils.exception_logger import log_exception
|
||||
|
|
@ -23,6 +28,9 @@ from plane.utils.paginator import BasePaginator
|
|||
from plane.utils.core.mixins import ReadReplicaControlMixin
|
||||
|
||||
|
||||
logger = logging.getLogger("plane.api")
|
||||
|
||||
|
||||
class TimezoneMixin:
|
||||
"""
|
||||
This enables timezone conversion according
|
||||
|
|
@ -152,3 +160,118 @@ class BaseAPIView(TimezoneMixin, GenericAPIView, ReadReplicaControlMixin, BasePa
|
|||
def expand(self):
|
||||
expand = [expand for expand in self.request.GET.get("expand", "").split(",") if expand]
|
||||
return expand if expand else None
|
||||
|
||||
|
||||
class BaseViewSet(TimezoneMixin, ReadReplicaControlMixin, ModelViewSet, BasePaginator):
|
||||
model = None
|
||||
|
||||
authentication_classes = [APIKeyAuthentication]
|
||||
permission_classes = [
|
||||
IsAuthenticated,
|
||||
]
|
||||
use_read_replica = False
|
||||
|
||||
def get_queryset(self):
|
||||
try:
|
||||
return self.model.objects.all()
|
||||
except Exception as e:
|
||||
log_exception(e)
|
||||
raise APIException("Please check the view", status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
def handle_exception(self, exc):
|
||||
"""
|
||||
Handle any exception that occurs, by returning an appropriate response,
|
||||
or re-raising the error.
|
||||
"""
|
||||
try:
|
||||
response = super().handle_exception(exc)
|
||||
return response
|
||||
except Exception as e:
|
||||
if isinstance(e, IntegrityError):
|
||||
log_exception(e)
|
||||
return Response(
|
||||
{"error": "The payload is not valid"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
if isinstance(e, ValidationError):
|
||||
logger.warning(
|
||||
"Validation Error",
|
||||
extra={
|
||||
"error_code": "VALIDATION_ERROR",
|
||||
"error_message": str(e),
|
||||
},
|
||||
)
|
||||
return Response(
|
||||
{"error": "Please provide valid detail"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
if isinstance(e, ObjectDoesNotExist):
|
||||
logger.warning(
|
||||
"Object Does Not Exist",
|
||||
extra={
|
||||
"error_code": "OBJECT_DOES_NOT_EXIST",
|
||||
"error_message": str(e),
|
||||
},
|
||||
)
|
||||
return Response(
|
||||
{"error": "The required object does not exist."},
|
||||
status=status.HTTP_404_NOT_FOUND,
|
||||
)
|
||||
|
||||
if isinstance(e, KeyError):
|
||||
logger.error(
|
||||
"Key Error",
|
||||
extra={
|
||||
"error_code": "KEY_ERROR",
|
||||
"error_message": str(e),
|
||||
},
|
||||
)
|
||||
return Response(
|
||||
{"error": "The required key does not exist."},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
log_exception(e)
|
||||
return Response(
|
||||
{"error": "Something went wrong please try again later"},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
)
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
try:
|
||||
response = super().dispatch(request, *args, **kwargs)
|
||||
|
||||
if settings.DEBUG:
|
||||
from django.db import connection
|
||||
|
||||
print(f"{request.method} - {request.get_full_path()} of Queries: {len(connection.queries)}")
|
||||
|
||||
return response
|
||||
except Exception as exc:
|
||||
response = self.handle_exception(exc)
|
||||
return response
|
||||
|
||||
@property
|
||||
def workspace_slug(self):
|
||||
return self.kwargs.get("slug", None)
|
||||
|
||||
@property
|
||||
def project_id(self):
|
||||
project_id = self.kwargs.get("project_id", None)
|
||||
if project_id:
|
||||
return project_id
|
||||
|
||||
if resolve(self.request.path_info).url_name == "project":
|
||||
return self.kwargs.get("pk", None)
|
||||
|
||||
@property
|
||||
def fields(self):
|
||||
fields = [field for field in self.request.GET.get("fields", "").split(",") if field]
|
||||
return fields if fields else None
|
||||
|
||||
@property
|
||||
def expand(self):
|
||||
expand = [expand for expand in self.request.GET.get("expand", "").split(",") if expand]
|
||||
return expand if expand else None
|
||||
|
|
|
|||
150
apps/api/plane/api/views/invite.py
Normal file
150
apps/api/plane/api/views/invite.py
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
# Third party imports
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import status
|
||||
from drf_spectacular.utils import (
|
||||
extend_schema,
|
||||
OpenApiResponse,
|
||||
OpenApiRequest,
|
||||
OpenApiParameter,
|
||||
OpenApiTypes,
|
||||
)
|
||||
|
||||
# Module imports
|
||||
from plane.api.views.base import BaseViewSet
|
||||
from plane.db.models import WorkspaceMemberInvite, Workspace
|
||||
from plane.api.serializers import WorkspaceInviteSerializer
|
||||
from plane.utils.permissions import WorkspaceOwnerPermission
|
||||
from plane.utils.openapi.parameters import WORKSPACE_SLUG_PARAMETER
|
||||
|
||||
|
||||
class WorkspaceInvitationsViewset(BaseViewSet):
|
||||
"""
|
||||
Endpoint for creating, listing and deleting workspace invites.
|
||||
"""
|
||||
|
||||
serializer_class = WorkspaceInviteSerializer
|
||||
model = WorkspaceMemberInvite
|
||||
|
||||
permission_classes = [
|
||||
WorkspaceOwnerPermission,
|
||||
]
|
||||
|
||||
def get_queryset(self):
|
||||
return self.filter_queryset(super().get_queryset().filter(workspace__slug=self.kwargs.get("slug")))
|
||||
|
||||
def get_object(self):
|
||||
return self.get_queryset().get(pk=self.kwargs.get("pk"))
|
||||
|
||||
@extend_schema(
|
||||
summary="List workspace invites",
|
||||
description="List all workspace invites for a workspace",
|
||||
responses={
|
||||
200: OpenApiResponse(
|
||||
description="Workspace invites",
|
||||
response=WorkspaceInviteSerializer(many=True),
|
||||
)
|
||||
},
|
||||
parameters=[
|
||||
WORKSPACE_SLUG_PARAMETER,
|
||||
],
|
||||
)
|
||||
def list(self, request, slug):
|
||||
workspace_member_invites = self.get_queryset()
|
||||
serializer = WorkspaceInviteSerializer(workspace_member_invites, many=True)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
@extend_schema(
|
||||
summary="Get workspace invite",
|
||||
description="Get a workspace invite by ID",
|
||||
responses={200: OpenApiResponse(description="Workspace invite", response=WorkspaceInviteSerializer)},
|
||||
parameters=[
|
||||
WORKSPACE_SLUG_PARAMETER,
|
||||
OpenApiParameter(
|
||||
name="pk",
|
||||
description="Workspace invite ID",
|
||||
required=True,
|
||||
type=OpenApiTypes.UUID,
|
||||
location=OpenApiParameter.PATH,
|
||||
),
|
||||
],
|
||||
)
|
||||
def retrieve(self, request, slug, pk):
|
||||
workspace_member_invite = self.get_object()
|
||||
serializer = WorkspaceInviteSerializer(workspace_member_invite)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
@extend_schema(
|
||||
summary="Create workspace invite",
|
||||
description="Create a workspace invite",
|
||||
responses={201: OpenApiResponse(description="Workspace invite", response=WorkspaceInviteSerializer)},
|
||||
request=OpenApiRequest(request=WorkspaceInviteSerializer),
|
||||
parameters=[
|
||||
WORKSPACE_SLUG_PARAMETER,
|
||||
],
|
||||
)
|
||||
def create(self, request, slug):
|
||||
workspace = Workspace.objects.get(slug=slug)
|
||||
serializer = WorkspaceInviteSerializer(data=request.data, context={"slug": slug})
|
||||
serializer.is_valid(raise_exception=True)
|
||||
serializer.save(workspace=workspace, created_by=request.user)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
|
||||
@extend_schema(
|
||||
summary="Update workspace invite",
|
||||
description="Update a workspace invite",
|
||||
responses={200: OpenApiResponse(description="Workspace invite", response=WorkspaceInviteSerializer)},
|
||||
request=OpenApiRequest(request=WorkspaceInviteSerializer),
|
||||
parameters=[
|
||||
WORKSPACE_SLUG_PARAMETER,
|
||||
OpenApiParameter(
|
||||
name="pk",
|
||||
description="Workspace invite ID",
|
||||
required=True,
|
||||
type=OpenApiTypes.UUID,
|
||||
location=OpenApiParameter.PATH,
|
||||
),
|
||||
],
|
||||
)
|
||||
def partial_update(self, request, slug, pk):
|
||||
workspace_member_invite = self.get_object()
|
||||
if request.data.get("email"):
|
||||
return Response(
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
data={"error": "Email cannot be updated after invite is created.", "code": "EMAIL_CANNOT_BE_UPDATED"},
|
||||
)
|
||||
serializer = WorkspaceInviteSerializer(
|
||||
workspace_member_invite, data=request.data, partial=True, context={"slug": slug}
|
||||
)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
serializer.save()
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
@extend_schema(
|
||||
summary="Delete workspace invite",
|
||||
description="Delete a workspace invite",
|
||||
responses={204: OpenApiResponse(description="Workspace invite deleted")},
|
||||
parameters=[
|
||||
WORKSPACE_SLUG_PARAMETER,
|
||||
OpenApiParameter(
|
||||
name="pk",
|
||||
description="Workspace invite ID",
|
||||
required=True,
|
||||
type=OpenApiTypes.UUID,
|
||||
location=OpenApiParameter.PATH,
|
||||
),
|
||||
],
|
||||
)
|
||||
def destroy(self, request, slug, pk):
|
||||
workspace_member_invite = self.get_object()
|
||||
if workspace_member_invite.accepted:
|
||||
return Response(
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
data={"error": "Invite already accepted", "code": "INVITE_ALREADY_ACCEPTED"},
|
||||
)
|
||||
if workspace_member_invite.responded_at:
|
||||
return Response(
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
data={"error": "Invite already responded", "code": "INVITE_ALREADY_RESPONDED"},
|
||||
)
|
||||
workspace_member_invite.delete()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
|
@ -1808,7 +1808,7 @@ class IssueAttachmentListCreateAPIEndpoint(BaseAPIView):
|
|||
request.user.id,
|
||||
project_id=project_id,
|
||||
issue=issue,
|
||||
allowed_roles=[ROLE.ADMIN.value, ROLE.MEMBER.value],
|
||||
allowed_roles=[ROLE.ADMIN.value, ROLE.MEMBER.value, ROLE.GUEST.value],
|
||||
allow_creator=True,
|
||||
):
|
||||
return Response(
|
||||
|
|
@ -1961,10 +1961,10 @@ class IssueAttachmentDetailAPIEndpoint(BaseAPIView):
|
|||
issue = Issue.objects.get(pk=issue_id, workspace__slug=slug, project_id=project_id)
|
||||
# if the request user is creator or admin then delete the attachment
|
||||
if not user_has_issue_permission(
|
||||
request.user,
|
||||
request.user.id,
|
||||
project_id=project_id,
|
||||
issue=issue,
|
||||
allowed_roles=[ROLE.ADMIN.value],
|
||||
allowed_roles=[ROLE.ADMIN.value, ROLE.MEMBER.value, ROLE.GUEST.value],
|
||||
allow_creator=True,
|
||||
):
|
||||
return Response(
|
||||
|
|
@ -2034,7 +2034,7 @@ class IssueAttachmentDetailAPIEndpoint(BaseAPIView):
|
|||
"""
|
||||
# if the user is part of the project then allow the download
|
||||
if not user_has_issue_permission(
|
||||
request.user,
|
||||
request.user.id,
|
||||
project_id=project_id,
|
||||
issue=None,
|
||||
allowed_roles=None,
|
||||
|
|
@ -2099,10 +2099,10 @@ class IssueAttachmentDetailAPIEndpoint(BaseAPIView):
|
|||
issue = Issue.objects.get(pk=issue_id, workspace__slug=slug, project_id=project_id)
|
||||
# if the user is creator or admin then allow the upload
|
||||
if not user_has_issue_permission(
|
||||
request.user,
|
||||
request.user.id,
|
||||
project_id=project_id,
|
||||
issue=issue,
|
||||
allowed_roles=[ROLE.ADMIN.value, ROLE.MEMBER.value],
|
||||
allowed_roles=[ROLE.ADMIN.value, ROLE.MEMBER.value, ROLE.GUEST.value],
|
||||
allow_creator=True,
|
||||
):
|
||||
return Response(
|
||||
|
|
|
|||
|
|
@ -4,13 +4,14 @@ from rest_framework import status
|
|||
from drf_spectacular.utils import (
|
||||
extend_schema,
|
||||
OpenApiResponse,
|
||||
OpenApiRequest,
|
||||
)
|
||||
|
||||
# Module imports
|
||||
from .base import BaseAPIView
|
||||
from plane.api.serializers import UserLiteSerializer
|
||||
from plane.api.serializers import UserLiteSerializer, ProjectMemberSerializer
|
||||
from plane.db.models import User, Workspace, WorkspaceMember, ProjectMember
|
||||
from plane.app.permissions import ProjectMemberPermission, WorkSpaceAdminPermission
|
||||
from plane.utils.permissions import ProjectMemberPermission, WorkSpaceAdminPermission, ProjectAdminPermission
|
||||
from plane.utils.openapi import (
|
||||
WORKSPACE_SLUG_PARAMETER,
|
||||
PROJECT_ID_PARAMETER,
|
||||
|
|
@ -86,11 +87,15 @@ class WorkspaceMemberAPIEndpoint(BaseAPIView):
|
|||
return Response(users_with_roles, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
# API endpoint to get and insert users inside the workspace
|
||||
class ProjectMemberAPIEndpoint(BaseAPIView):
|
||||
class ProjectMemberListCreateAPIEndpoint(BaseAPIView):
|
||||
permission_classes = [ProjectMemberPermission]
|
||||
use_read_replica = True
|
||||
|
||||
def get_permissions(self):
|
||||
if self.request.method == "GET":
|
||||
return [ProjectMemberPermission()]
|
||||
return [ProjectAdminPermission()]
|
||||
|
||||
@extend_schema(
|
||||
operation_id="get_project_members",
|
||||
summary="List project members",
|
||||
|
|
@ -129,5 +134,86 @@ class ProjectMemberAPIEndpoint(BaseAPIView):
|
|||
|
||||
# Get all the users that are present inside the workspace
|
||||
users = UserLiteSerializer(User.objects.filter(id__in=project_members), many=True).data
|
||||
|
||||
return Response(users, status=status.HTTP_200_OK)
|
||||
|
||||
@extend_schema(
|
||||
operation_id="create_project_member",
|
||||
summary="Create project member",
|
||||
description="Create a new project member",
|
||||
tags=["Members"],
|
||||
parameters=[WORKSPACE_SLUG_PARAMETER, PROJECT_ID_PARAMETER],
|
||||
responses={201: OpenApiResponse(description="Project member created", response=ProjectMemberSerializer)},
|
||||
request=OpenApiRequest(request=ProjectMemberSerializer),
|
||||
)
|
||||
def post(self, request, slug, project_id):
|
||||
serializer = ProjectMemberSerializer(data=request.data, context={"slug": slug})
|
||||
serializer.is_valid(raise_exception=True)
|
||||
serializer.save(project_id=project_id)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
|
||||
|
||||
# API endpoint to get and update a project member
|
||||
class ProjectMemberDetailAPIEndpoint(ProjectMemberListCreateAPIEndpoint):
|
||||
|
||||
@extend_schema(
|
||||
operation_id="get_project_member",
|
||||
summary="Get project member",
|
||||
description="Retrieve a project member by ID.",
|
||||
tags=["Members"],
|
||||
parameters=[WORKSPACE_SLUG_PARAMETER, PROJECT_ID_PARAMETER],
|
||||
responses={
|
||||
200: OpenApiResponse(description="Project member", response=ProjectMemberSerializer),
|
||||
401: UNAUTHORIZED_RESPONSE,
|
||||
403: FORBIDDEN_RESPONSE,
|
||||
404: PROJECT_NOT_FOUND_RESPONSE,
|
||||
},
|
||||
)
|
||||
# Get a project member by ID
|
||||
def get(self, request, slug, project_id, pk):
|
||||
"""Get project member
|
||||
|
||||
Retrieve a project member by ID.
|
||||
Returns a project member with their project-specific roles and access levels.
|
||||
"""
|
||||
# Check if the workspace exists
|
||||
if not Workspace.objects.filter(slug=slug).exists():
|
||||
return Response(
|
||||
{"error": "Provided workspace does not exist"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
# Get the workspace members that are present inside the workspace
|
||||
project_members = ProjectMember.objects.get(project_id=project_id, workspace__slug=slug, pk=pk)
|
||||
user = User.objects.get(id=project_members.member_id)
|
||||
user = UserLiteSerializer(user).data
|
||||
return Response(user, status=status.HTTP_200_OK)
|
||||
|
||||
@extend_schema(
|
||||
operation_id="update_project_member",
|
||||
summary="Update project member",
|
||||
description="Update a project member",
|
||||
tags=["Members"],
|
||||
parameters=[WORKSPACE_SLUG_PARAMETER, PROJECT_ID_PARAMETER],
|
||||
responses={200: OpenApiResponse(description="Project member updated", response=ProjectMemberSerializer)},
|
||||
request=OpenApiRequest(request=ProjectMemberSerializer),
|
||||
)
|
||||
def patch(self, request, slug, project_id, pk):
|
||||
project_member = ProjectMember.objects.get(project_id=project_id, workspace__slug=slug, pk=pk)
|
||||
serializer = ProjectMemberSerializer(project_member, data=request.data, partial=True, context={"slug": slug})
|
||||
serializer.is_valid(raise_exception=True)
|
||||
serializer.save()
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
@extend_schema(
|
||||
operation_id="delete_project_member",
|
||||
summary="Delete project member",
|
||||
description="Delete a project member",
|
||||
tags=["Members"],
|
||||
parameters=[WORKSPACE_SLUG_PARAMETER, PROJECT_ID_PARAMETER],
|
||||
responses={204: OpenApiResponse(description="Project member deleted")},
|
||||
)
|
||||
def delete(self, request, slug, project_id, pk):
|
||||
project_member = ProjectMember.objects.get(project_id=project_id, workspace__slug=slug, pk=pk)
|
||||
project_member.is_active = False
|
||||
project_member.save()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue