351 lines
13 KiB
Python
351 lines
13 KiB
Python
from django.db.models import Q
|
|
from django.shortcuts import get_object_or_404
|
|
from rest_framework import status
|
|
from rest_framework.exceptions import PermissionDenied
|
|
from rest_framework.response import Response
|
|
from rest_framework.filters import OrderingFilter, SearchFilter
|
|
from django_filters.rest_framework import DjangoFilterBackend
|
|
from rest_framework.viewsets import ModelViewSet
|
|
from rest_framework.permissions import IsAuthenticated
|
|
from rest_framework.parsers import FormParser, MultiPartParser, JSONParser
|
|
|
|
from apps.notifications.services import (
|
|
notify_workspace_membership_added,
|
|
notify_workspace_membership_deactivated,
|
|
notify_workspace_membership_removed,
|
|
notify_workspace_membership_role_changed,
|
|
)
|
|
from apps.workspaces.api.permissions import (
|
|
CanWorkspaceManageMembers,
|
|
IsWorkspaceAdmin,
|
|
IsWorkspaceMember,
|
|
IsWorkspaceOwner,
|
|
)
|
|
from apps.workspaces.api.serializers import (
|
|
PriceUnitSerializer,
|
|
WorkspaceMembershipSerializer,
|
|
WorkspaceSerializer,
|
|
WorkspaceUserRateSerializer,
|
|
WorkspaceUserRateCreateSerializer,
|
|
WorkspaceUserRateUpdateSerializer,
|
|
)
|
|
from apps.workspaces.api.filters import WorkspaceFilter, WorkspaceMembershipFilter
|
|
from apps.workspaces.models import PriceUnit, Workspace, WorkspaceMembership, WorkspaceUserRate
|
|
from apps.workspaces.services import (
|
|
WORKSPACE_MEMBERS_VIEW,
|
|
WORKSPACE_EDIT,
|
|
WORKSPACE_VIEW,
|
|
can_assign_workspace_role,
|
|
can_change_workspace_membership,
|
|
has_workspace_capability,
|
|
upsert_workspace_user_rate,
|
|
update_workspace_user_rate,
|
|
)
|
|
from core.paginations.limit_offset import CustomLimitOffsetPagination
|
|
|
|
|
|
class WorkspaceViewSet(ModelViewSet):
|
|
serializer_class = WorkspaceSerializer
|
|
parser_classes = [MultiPartParser, FormParser, JSONParser]
|
|
pagination_class = CustomLimitOffsetPagination
|
|
filter_backends = (DjangoFilterBackend, OrderingFilter, SearchFilter)
|
|
filterset_class = WorkspaceFilter
|
|
search_fields = ("name", "description")
|
|
ordering_fields = ("created_at", "updated_at", "name")
|
|
ordering = ("-updated_at", "-created_at")
|
|
|
|
def get_queryset(self):
|
|
user = self.request.user
|
|
if not user.is_authenticated:
|
|
return Workspace.objects.none()
|
|
|
|
return Workspace.objects.filter(
|
|
Q(owner=user) |
|
|
Q(memberships__user=user, memberships__is_active=True)
|
|
).distinct()
|
|
|
|
def get_permissions(self):
|
|
if self.action in ["list", "retrieve"]:
|
|
return [IsAuthenticated(), IsWorkspaceMember()]
|
|
if self.action in ["update", "partial_update"]:
|
|
return [IsAuthenticated(), IsWorkspaceAdmin()]
|
|
|
|
elif self.action == "destroy":
|
|
return [IsAuthenticated(), IsWorkspaceOwner()]
|
|
|
|
return [IsAuthenticated()]
|
|
|
|
def perform_create(self, serializer):
|
|
serializer.save(owner=self.request.user)
|
|
|
|
|
|
class WorkspaceMembershipViewSet(ModelViewSet):
|
|
serializer_class = WorkspaceMembershipSerializer
|
|
pagination_class = CustomLimitOffsetPagination
|
|
filter_backends = (DjangoFilterBackend, OrderingFilter, SearchFilter)
|
|
filterset_class = WorkspaceMembershipFilter
|
|
search_fields = (
|
|
"user__mobile",
|
|
"user__email",
|
|
"user__first_name",
|
|
"user__last_name",
|
|
"workspace__name"
|
|
)
|
|
ordering_fields = ("joined_at", "created_at", "role")
|
|
ordering = ("-created_at",)
|
|
|
|
def get_queryset(self):
|
|
user = self.request.user
|
|
if not user.is_authenticated:
|
|
return WorkspaceMembership.objects.none()
|
|
|
|
return WorkspaceMembership.objects.filter(
|
|
Q(workspace__owner=user) |
|
|
Q(workspace__memberships__user=user, workspace__memberships__is_active=True)
|
|
).distinct()
|
|
|
|
def get_permissions(self):
|
|
if self.action in ["list", "retrieve"]:
|
|
return [IsAuthenticated()]
|
|
if self.action in ["create", "update", "partial_update"]:
|
|
return [IsAuthenticated(), CanWorkspaceManageMembers()]
|
|
if self.action in ["destroy"]:
|
|
return [IsAuthenticated(), CanWorkspaceManageMembers()]
|
|
|
|
return [IsAuthenticated()]
|
|
|
|
def list(self, request, *args, **kwargs):
|
|
workspace_id = request.query_params.get("workspace")
|
|
if not workspace_id:
|
|
return Response(
|
|
{"detail": "workspace query parameter is required."},
|
|
status=status.HTTP_400_BAD_REQUEST,
|
|
)
|
|
|
|
workspace = get_object_or_404(Workspace, id=workspace_id, is_deleted=False)
|
|
if not has_workspace_capability(request.user, workspace, WORKSPACE_VIEW):
|
|
return Response(
|
|
{"detail": "You do not have permission to view workspace members."},
|
|
status=status.HTTP_403_FORBIDDEN,
|
|
)
|
|
|
|
return super().list(request, *args, **kwargs)
|
|
|
|
def create(self, request, *args, **kwargs):
|
|
"""
|
|
Overridden to check permissions manually.
|
|
Because the membership object doesn't exist yet, standard DRF object-level
|
|
permissions won't catch payload-level workspace violations.
|
|
"""
|
|
workspace_id = request.data.get("workspace")
|
|
if not workspace_id:
|
|
return Response(
|
|
{"workspace": ["This field is required."]},
|
|
status=status.HTTP_400_BAD_REQUEST
|
|
)
|
|
|
|
workspace = get_object_or_404(Workspace, id=workspace_id)
|
|
|
|
permission = IsWorkspaceAdmin()
|
|
if not permission.has_object_permission(request, self, workspace):
|
|
return Response(
|
|
{"detail": "You must be a Workspace Admin or Owner to add members."},
|
|
status=status.HTTP_403_FORBIDDEN
|
|
)
|
|
|
|
requested_role = request.data.get("role")
|
|
if requested_role and not can_assign_workspace_role(
|
|
request.user,
|
|
workspace,
|
|
requested_role,
|
|
):
|
|
return Response(
|
|
{"detail": "You do not have permission to assign this workspace role."},
|
|
status=status.HTTP_403_FORBIDDEN,
|
|
)
|
|
|
|
serializer = self.get_serializer(data=request.data)
|
|
serializer.is_valid(raise_exception=True)
|
|
membership = serializer.save()
|
|
notify_workspace_membership_added(
|
|
actor=request.user,
|
|
recipient=membership.user,
|
|
workspace=membership.workspace,
|
|
role=membership.role,
|
|
)
|
|
return Response(
|
|
WorkspaceMembershipSerializer(membership, context=self.get_serializer_context()).data,
|
|
status=status.HTTP_201_CREATED,
|
|
)
|
|
|
|
def update(self, request, *args, **kwargs):
|
|
partial = kwargs.pop("partial", False)
|
|
membership = self.get_object()
|
|
previous_role = membership.role
|
|
previous_is_active = membership.is_active
|
|
|
|
requested_role = request.data.get("role")
|
|
if not can_change_workspace_membership(
|
|
request.user,
|
|
membership,
|
|
new_role=requested_role,
|
|
):
|
|
return Response(
|
|
{"detail": "You do not have permission to change this workspace membership."},
|
|
status=status.HTTP_403_FORBIDDEN,
|
|
)
|
|
|
|
serializer = self.get_serializer(membership, data=request.data, partial=partial)
|
|
serializer.is_valid(raise_exception=True)
|
|
updated_membership = serializer.save()
|
|
|
|
if not previous_is_active and updated_membership.is_active:
|
|
notify_workspace_membership_added(
|
|
actor=request.user,
|
|
recipient=updated_membership.user,
|
|
workspace=updated_membership.workspace,
|
|
role=updated_membership.role,
|
|
)
|
|
elif previous_is_active and not updated_membership.is_active:
|
|
notify_workspace_membership_deactivated(
|
|
actor=request.user,
|
|
recipient=updated_membership.user,
|
|
workspace=updated_membership.workspace,
|
|
role=previous_role,
|
|
)
|
|
elif previous_role != updated_membership.role:
|
|
notify_workspace_membership_role_changed(
|
|
actor=request.user,
|
|
recipient=updated_membership.user,
|
|
workspace=updated_membership.workspace,
|
|
previous_role=previous_role,
|
|
new_role=updated_membership.role,
|
|
)
|
|
|
|
return Response(
|
|
WorkspaceMembershipSerializer(
|
|
updated_membership,
|
|
context=self.get_serializer_context(),
|
|
).data,
|
|
status=status.HTTP_200_OK,
|
|
)
|
|
|
|
def partial_update(self, request, *args, **kwargs):
|
|
kwargs["partial"] = True
|
|
return self.update(request, *args, **kwargs)
|
|
|
|
def destroy(self, request, *args, **kwargs):
|
|
membership = self.get_object()
|
|
if not can_change_workspace_membership(request.user, membership):
|
|
return Response(
|
|
{"detail": "You do not have permission to remove this workspace membership."},
|
|
status=status.HTTP_403_FORBIDDEN,
|
|
)
|
|
recipient = membership.user
|
|
workspace = membership.workspace
|
|
role = membership.role
|
|
membership.delete()
|
|
notify_workspace_membership_removed(
|
|
actor=request.user,
|
|
recipient=recipient,
|
|
workspace=workspace,
|
|
role=role,
|
|
)
|
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
|
|
|
|
|
class PriceUnitViewSet(ModelViewSet):
|
|
serializer_class = PriceUnitSerializer
|
|
permission_classes = [IsAuthenticated]
|
|
http_method_names = ["get", "head", "options"]
|
|
pagination_class = None
|
|
filter_backends = (SearchFilter, OrderingFilter)
|
|
search_fields = ("code", "name", "local_name", "symbol")
|
|
ordering_fields = ("code", "name")
|
|
ordering = ("code",)
|
|
|
|
def get_queryset(self):
|
|
return PriceUnit.objects.filter(is_deleted=False)
|
|
|
|
|
|
class WorkspaceUserRateViewSet(ModelViewSet):
|
|
serializer_class = WorkspaceUserRateSerializer
|
|
pagination_class = CustomLimitOffsetPagination
|
|
filter_backends = (DjangoFilterBackend, OrderingFilter)
|
|
filterset_fields = ("workspace", "user", "currency")
|
|
ordering_fields = ("effective_from", "updated_at", "created_at")
|
|
ordering = ("-effective_from", "-updated_at")
|
|
|
|
def get_queryset(self):
|
|
user = self.request.user
|
|
if not user.is_authenticated:
|
|
return WorkspaceUserRate.objects.none()
|
|
return WorkspaceUserRate.objects.filter(
|
|
workspace__memberships__user=user,
|
|
workspace__memberships__is_active=True,
|
|
is_deleted=False,
|
|
).distinct()
|
|
|
|
def get_serializer_class(self):
|
|
if self.action == "create":
|
|
return WorkspaceUserRateCreateSerializer
|
|
if self.action in ["update", "partial_update"]:
|
|
return WorkspaceUserRateUpdateSerializer
|
|
return WorkspaceUserRateSerializer
|
|
|
|
def _ensure_manage_access(self, user, workspace):
|
|
if not has_workspace_capability(user, workspace, WORKSPACE_EDIT):
|
|
raise PermissionDenied("You do not have permission to manage workspace rates.")
|
|
|
|
def list(self, request, *args, **kwargs):
|
|
workspace_id = request.query_params.get("workspace")
|
|
if not workspace_id:
|
|
return Response(
|
|
{"detail": "workspace query parameter is required."},
|
|
status=status.HTTP_400_BAD_REQUEST,
|
|
)
|
|
workspace = get_object_or_404(Workspace, id=workspace_id, is_deleted=False)
|
|
self._ensure_manage_access(request.user, workspace)
|
|
return super().list(request, *args, **kwargs)
|
|
|
|
def create(self, request, *args, **kwargs):
|
|
serializer = self.get_serializer(data=request.data)
|
|
serializer.is_valid(raise_exception=True)
|
|
workspace = get_object_or_404(
|
|
Workspace,
|
|
id=serializer.validated_data["workspace_id"],
|
|
is_deleted=False,
|
|
)
|
|
self._ensure_manage_access(request.user, workspace)
|
|
rate = upsert_workspace_user_rate(
|
|
workspace=workspace,
|
|
user_id=serializer.validated_data["user_id"],
|
|
hourly_rate=serializer.validated_data["hourly_rate"],
|
|
currency=serializer.validated_data.get("currency", "USD"),
|
|
)
|
|
return Response(
|
|
WorkspaceUserRateSerializer(rate, context=self.get_serializer_context()).data,
|
|
status=status.HTTP_201_CREATED,
|
|
)
|
|
|
|
def update(self, request, *args, **kwargs):
|
|
rate = self.get_object()
|
|
self._ensure_manage_access(request.user, rate.workspace)
|
|
serializer = self.get_serializer(data=request.data, partial=kwargs.pop("partial", False))
|
|
serializer.is_valid(raise_exception=True)
|
|
updated_rate = update_workspace_user_rate(rate, **serializer.validated_data)
|
|
return Response(
|
|
WorkspaceUserRateSerializer(updated_rate, context=self.get_serializer_context()).data,
|
|
status=status.HTTP_200_OK,
|
|
)
|
|
|
|
def partial_update(self, request, *args, **kwargs):
|
|
kwargs["partial"] = True
|
|
return self.update(request, *args, **kwargs)
|
|
|
|
def destroy(self, request, *args, **kwargs):
|
|
rate = self.get_object()
|
|
self._ensure_manage_access(request.user, rate.workspace)
|
|
rate.delete()
|
|
return Response(status=status.HTTP_204_NO_CONTENT)
|