feat(workspaces): add current user rates endpoint

This commit is contained in:
2026-05-23 19:43:10 +03:30
parent 181a135df9
commit 0d6c6a4f09
2 changed files with 133 additions and 4 deletions

View File

@@ -3,7 +3,8 @@ 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 rest_framework.filters import OrderingFilter, SearchFilter
from rest_framework.decorators import action
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework.viewsets import ModelViewSet
from rest_framework.permissions import IsAuthenticated
@@ -15,6 +16,8 @@ from apps.notifications.services import (
notify_workspace_membership_removed,
notify_workspace_membership_role_changed,
)
from apps.projects.models import ProjectUserRate
from apps.projects.services.access import filter_projects_for_user
from apps.workspaces.api.permissions import (
CanWorkspaceManageMembers,
IsWorkspaceAdmin,
@@ -78,6 +81,8 @@ class WorkspaceViewSet(ModelViewSet):
def get_permissions(self):
if self.action in ["list", "retrieve"]:
return [IsAuthenticated(), IsWorkspaceMember()]
if self.action == "my_rates":
return [IsAuthenticated()]
if self.action in ["update", "partial_update"]:
return [IsAuthenticated(), IsWorkspaceAdmin()]
@@ -86,8 +91,86 @@ class WorkspaceViewSet(ModelViewSet):
return [IsAuthenticated()]
def perform_create(self, serializer):
serializer.save(owner=self.request.user)
def perform_create(self, serializer):
serializer.save(owner=self.request.user)
@action(detail=True, methods=["get"], url_path="my-rates")
def my_rates(self, request, pk=None):
workspace = self.get_object()
if not has_workspace_capability(request.user, workspace, WORKSPACE_VIEW):
raise PermissionDenied("You do not have access to this workspace.")
def serialize_rate(rate):
if not rate:
return None
unit = PriceUnit.objects.filter(code=rate.currency, is_deleted=False).first()
return {
"id": str(rate.id),
"hourly_rate": str(rate.hourly_rate),
"currency": rate.currency,
"price_unit": PriceUnitSerializer(unit).data if unit else None,
"effective_from": rate.effective_from.isoformat() if rate.effective_from else None,
}
workspace_rate = (
WorkspaceUserRate.objects.filter(
workspace=workspace,
user=request.user,
is_deleted=False,
)
.order_by("-effective_from", "-updated_at")
.first()
)
accessible_projects = list(
filter_projects_for_user(
request.user,
workspace.projects.filter(is_deleted=False).select_related("client"),
).order_by("client__name", "name")
)
accessible_project_ids = [project.id for project in accessible_projects]
project_rates_by_project_id = {}
for rate in (
ProjectUserRate.objects.filter(
project_id__in=accessible_project_ids,
user=request.user,
is_active=True,
is_deleted=False,
)
.select_related("project", "project__client")
.order_by("project_id", "-effective_from", "-updated_at")
):
project_rates_by_project_id.setdefault(str(rate.project_id), rate)
payload = {
"workspace": {
"id": str(workspace.id),
"name": workspace.name,
},
"workspace_rate": serialize_rate(workspace_rate),
"accessible_project_count": len(accessible_projects),
"project_rates": [
{
"project": {
"id": str(project.id),
"name": project.name,
"client": (
{"id": str(project.client_id), "name": project.client.name}
if project.client_id and project.client
else None
),
},
"rate": serialize_rate(project_rates_by_project_id[str(project.id)]),
}
for project in accessible_projects
if str(project.id) in project_rates_by_project_id
],
}
payload["project_override_count"] = len(payload["project_rates"])
payload["workspace_fallback_project_count"] = max(
payload["accessible_project_count"] - payload["project_override_count"],
0,
)
return Response(payload)
class WorkspaceMembershipViewSet(ModelViewSet):

View File

@@ -4,6 +4,7 @@ from django.core.cache import cache
from django.test import TestCase
from rest_framework.test import APITestCase
from apps.projects.models import Project, ProjectAccess, ProjectUserRate
from apps.users.models import User
from apps.workspaces.api.permissions import (
CanWorkspaceManageMembers,
@@ -11,7 +12,7 @@ from apps.workspaces.api.permissions import (
IsWorkspaceMember,
IsWorkspaceOwner,
)
from apps.workspaces.models import Workspace, WorkspaceMembership
from apps.workspaces.models import PriceUnit, Workspace, WorkspaceMembership, WorkspaceUserRate
class WorkspacePermissionTests(TestCase):
@@ -189,3 +190,48 @@ class WorkspaceMembershipCacheTests(APITestCase):
target = next(item for item in fresh_response.data["items"] if item["id"] == str(self.membership.id))
self.assertEqual(target["role"], WorkspaceMembership.Role.GUEST)
self.assertFalse(target["is_active"])
class WorkspaceMyRatesApiTests(APITestCase):
@classmethod
def setUpTestData(cls):
cls.owner = User.objects.create_user(mobile="09127770101", password="secret123")
cls.member = User.objects.create_user(mobile="09127770102", password="secret123")
cls.workspace = Workspace.objects.create(name="Rates View", owner=cls.owner)
WorkspaceMembership.objects.create(
workspace=cls.workspace,
user=cls.member,
role=WorkspaceMembership.Role.MEMBER,
is_active=True,
)
PriceUnit.objects.create(code="USD", name="US Dollar", local_name="Dollar", symbol="$")
cls.project = Project.objects.create(workspace=cls.workspace, name="Mobile App")
ProjectAccess.objects.create(project=cls.project, user=cls.member)
WorkspaceUserRate.objects.create(
workspace=cls.workspace,
user=cls.member,
hourly_rate="10.00",
currency="USD",
effective_from=cls.workspace.created_at,
is_active=True,
)
ProjectUserRate.objects.create(
project=cls.project,
user=cls.member,
hourly_rate="18.00",
currency="USD",
effective_from=cls.workspace.created_at,
is_active=True,
)
def test_member_can_view_own_workspace_and_project_rates(self):
self.client.force_authenticate(user=self.member)
response = self.client.get(f"/api/workspaces/{self.workspace.id}/my-rates/")
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data["workspace_rate"]["hourly_rate"], "10.00")
self.assertEqual(response.data["project_override_count"], 1)
self.assertEqual(response.data["workspace_fallback_project_count"], 0)
self.assertEqual(response.data["project_rates"][0]["project"]["name"], "Mobile App")
self.assertEqual(response.data["project_rates"][0]["rate"]["hourly_rate"], "18.00")