feat(projects): support implicit-access roles in rates modal
This commit is contained in:
@@ -24,9 +24,10 @@ from apps.projects.services.access import (
|
||||
build_project_access_items,
|
||||
ensure_workspace_project_access,
|
||||
filter_projects_for_user,
|
||||
get_access_managed_membership,
|
||||
get_project_access_target_membership,
|
||||
grant_project_accesses,
|
||||
revoke_project_accesses,
|
||||
user_has_project_access,
|
||||
)
|
||||
from apps.projects.services.rates import get_current_project_user_rate, remove_project_user_rate, upsert_project_user_rate
|
||||
from apps.projects.services.projects import (
|
||||
@@ -182,7 +183,7 @@ class ProjectViewSet(ModelViewSet):
|
||||
is_deleted=False,
|
||||
)
|
||||
ensure_workspace_project_access(request.user, workspace)
|
||||
membership = get_access_managed_membership(workspace, str(serializer.validated_data["user"]))
|
||||
membership = get_project_access_target_membership(workspace, str(serializer.validated_data["user"]))
|
||||
|
||||
return Response(
|
||||
{
|
||||
@@ -207,7 +208,7 @@ class ProjectViewSet(ModelViewSet):
|
||||
id=serializer.validated_data["workspace"],
|
||||
is_deleted=False,
|
||||
)
|
||||
membership = get_access_managed_membership(workspace, str(serializer.validated_data["user"]))
|
||||
membership = get_project_access_target_membership(workspace, str(serializer.validated_data["user"]))
|
||||
changed = grant_project_accesses(
|
||||
actor=request.user,
|
||||
workspace=workspace,
|
||||
@@ -226,7 +227,7 @@ class ProjectViewSet(ModelViewSet):
|
||||
id=serializer.validated_data["workspace"],
|
||||
is_deleted=False,
|
||||
)
|
||||
membership = get_access_managed_membership(workspace, str(serializer.validated_data["user"]))
|
||||
membership = get_project_access_target_membership(workspace, str(serializer.validated_data["user"]))
|
||||
changed = revoke_project_accesses(
|
||||
actor=request.user,
|
||||
workspace=workspace,
|
||||
@@ -246,7 +247,7 @@ class ProjectViewSet(ModelViewSet):
|
||||
is_deleted=False,
|
||||
)
|
||||
ensure_workspace_project_access(request.user, workspace)
|
||||
membership = get_access_managed_membership(workspace, str(serializer.validated_data["user"]))
|
||||
membership = get_project_access_target_membership(workspace, str(serializer.validated_data["user"]))
|
||||
project = get_object_or_404(
|
||||
Project,
|
||||
id=serializer.validated_data["project"],
|
||||
@@ -254,7 +255,7 @@ class ProjectViewSet(ModelViewSet):
|
||||
is_deleted=False,
|
||||
)
|
||||
|
||||
has_access = membership.user.project_accesses.filter(project=project).exists()
|
||||
has_access = user_has_project_access(membership.user, project)
|
||||
if not has_access:
|
||||
return Response(
|
||||
{"detail": "Grant project access before setting a project-specific rate."},
|
||||
|
||||
@@ -66,7 +66,7 @@ def ensure_workspace_project_access(user, workspace: Workspace) -> None:
|
||||
raise PermissionDenied("You do not have permission to manage project access in this workspace.")
|
||||
|
||||
|
||||
def get_access_managed_membership(workspace: Workspace, user_id: str) -> WorkspaceMembership:
|
||||
def get_project_access_target_membership(workspace: Workspace, user_id: str) -> WorkspaceMembership:
|
||||
membership = WorkspaceMembership.objects.filter(
|
||||
workspace=workspace,
|
||||
user_id=user_id,
|
||||
@@ -75,8 +75,6 @@ def get_access_managed_membership(workspace: Workspace, user_id: str) -> Workspa
|
||||
).select_related("user").first()
|
||||
if not membership:
|
||||
raise ValidationError({"user": "Selected user is not an active member of this workspace."})
|
||||
if membership.role not in PROJECT_ACCESS_MANAGED_ROLES:
|
||||
raise ValidationError({"user": "Owners and admins have implicit access to all projects."})
|
||||
return membership
|
||||
|
||||
|
||||
@@ -146,7 +144,7 @@ def build_project_access_items(*, workspace: Workspace, target_user) -> list[dic
|
||||
return [
|
||||
build_project_access_item(
|
||||
project=project,
|
||||
has_access=str(project.id) in explicit_access_ids,
|
||||
has_access=user_has_project_access(target_user, project) if user_has_implicit_project_access(target_user, workspace) else str(project.id) in explicit_access_ids,
|
||||
workspace_rate=workspace_rate,
|
||||
project_rate=project_rates.get(str(project.id)),
|
||||
)
|
||||
@@ -156,7 +154,9 @@ def build_project_access_items(*, workspace: Workspace, target_user) -> list[dic
|
||||
|
||||
def grant_project_accesses(*, actor, workspace: Workspace, target_user, project_ids: list[str]) -> int:
|
||||
ensure_workspace_project_access(actor, workspace)
|
||||
get_access_managed_membership(workspace, str(target_user.id))
|
||||
membership = get_project_access_target_membership(workspace, str(target_user.id))
|
||||
if membership.role not in PROJECT_ACCESS_MANAGED_ROLES:
|
||||
raise ValidationError({"user": "Owners and admins already have access to all projects."})
|
||||
|
||||
projects = list(Project.objects.filter(workspace=workspace, id__in=project_ids, is_deleted=False))
|
||||
if len(projects) != len(set(project_ids)):
|
||||
@@ -175,7 +175,9 @@ def grant_project_accesses(*, actor, workspace: Workspace, target_user, project_
|
||||
|
||||
def revoke_project_accesses(*, actor, workspace: Workspace, target_user, project_ids: list[str]) -> int:
|
||||
ensure_workspace_project_access(actor, workspace)
|
||||
get_access_managed_membership(workspace, str(target_user.id))
|
||||
membership = get_project_access_target_membership(workspace, str(target_user.id))
|
||||
if membership.role not in PROJECT_ACCESS_MANAGED_ROLES:
|
||||
raise ValidationError({"user": "Owners and admins always keep project access."})
|
||||
|
||||
accesses = list(
|
||||
ProjectAccess.objects.filter(
|
||||
|
||||
@@ -191,3 +191,29 @@ class ProjectViewTests(APITestCase):
|
||||
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertIn("Grant project access", response.data["detail"])
|
||||
|
||||
def test_owner_access_state_marks_all_projects_as_accessible_and_allows_project_rate_override(self):
|
||||
self.client.force_authenticate(user=self.owner)
|
||||
|
||||
access_response = self.client.get(
|
||||
"/api/projects/access/",
|
||||
{"workspace": str(self.workspace.id), "user": str(self.owner.id)},
|
||||
)
|
||||
self.assertEqual(access_response.status_code, 200)
|
||||
self.assertTrue(all(item["has_access"] for item in access_response.data["items"]))
|
||||
|
||||
save_response = self.client.post(
|
||||
"/api/projects/access/rate/",
|
||||
{
|
||||
"workspace": str(self.workspace.id),
|
||||
"user": str(self.owner.id),
|
||||
"project": str(self.first_project.id),
|
||||
"hourly_rate": "60.00",
|
||||
"currency": "USD",
|
||||
},
|
||||
format="json",
|
||||
)
|
||||
|
||||
self.assertEqual(save_response.status_code, 200)
|
||||
self.assertEqual(save_response.data["item"]["project_rate"]["hourly_rate"], "60.00")
|
||||
self.assertTrue(ProjectUserRate.objects.filter(project=self.first_project, user=self.owner, is_deleted=False).exists())
|
||||
|
||||
Reference in New Issue
Block a user