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,
|
build_project_access_items,
|
||||||
ensure_workspace_project_access,
|
ensure_workspace_project_access,
|
||||||
filter_projects_for_user,
|
filter_projects_for_user,
|
||||||
get_access_managed_membership,
|
get_project_access_target_membership,
|
||||||
grant_project_accesses,
|
grant_project_accesses,
|
||||||
revoke_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.rates import get_current_project_user_rate, remove_project_user_rate, upsert_project_user_rate
|
||||||
from apps.projects.services.projects import (
|
from apps.projects.services.projects import (
|
||||||
@@ -182,7 +183,7 @@ class ProjectViewSet(ModelViewSet):
|
|||||||
is_deleted=False,
|
is_deleted=False,
|
||||||
)
|
)
|
||||||
ensure_workspace_project_access(request.user, workspace)
|
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(
|
return Response(
|
||||||
{
|
{
|
||||||
@@ -207,7 +208,7 @@ class ProjectViewSet(ModelViewSet):
|
|||||||
id=serializer.validated_data["workspace"],
|
id=serializer.validated_data["workspace"],
|
||||||
is_deleted=False,
|
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(
|
changed = grant_project_accesses(
|
||||||
actor=request.user,
|
actor=request.user,
|
||||||
workspace=workspace,
|
workspace=workspace,
|
||||||
@@ -226,7 +227,7 @@ class ProjectViewSet(ModelViewSet):
|
|||||||
id=serializer.validated_data["workspace"],
|
id=serializer.validated_data["workspace"],
|
||||||
is_deleted=False,
|
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(
|
changed = revoke_project_accesses(
|
||||||
actor=request.user,
|
actor=request.user,
|
||||||
workspace=workspace,
|
workspace=workspace,
|
||||||
@@ -246,7 +247,7 @@ class ProjectViewSet(ModelViewSet):
|
|||||||
is_deleted=False,
|
is_deleted=False,
|
||||||
)
|
)
|
||||||
ensure_workspace_project_access(request.user, workspace)
|
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 = get_object_or_404(
|
||||||
Project,
|
Project,
|
||||||
id=serializer.validated_data["project"],
|
id=serializer.validated_data["project"],
|
||||||
@@ -254,7 +255,7 @@ class ProjectViewSet(ModelViewSet):
|
|||||||
is_deleted=False,
|
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:
|
if not has_access:
|
||||||
return Response(
|
return Response(
|
||||||
{"detail": "Grant project access before setting a project-specific rate."},
|
{"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.")
|
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(
|
membership = WorkspaceMembership.objects.filter(
|
||||||
workspace=workspace,
|
workspace=workspace,
|
||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
@@ -75,8 +75,6 @@ def get_access_managed_membership(workspace: Workspace, user_id: str) -> Workspa
|
|||||||
).select_related("user").first()
|
).select_related("user").first()
|
||||||
if not membership:
|
if not membership:
|
||||||
raise ValidationError({"user": "Selected user is not an active member of this workspace."})
|
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
|
return membership
|
||||||
|
|
||||||
|
|
||||||
@@ -146,7 +144,7 @@ def build_project_access_items(*, workspace: Workspace, target_user) -> list[dic
|
|||||||
return [
|
return [
|
||||||
build_project_access_item(
|
build_project_access_item(
|
||||||
project=project,
|
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,
|
workspace_rate=workspace_rate,
|
||||||
project_rate=project_rates.get(str(project.id)),
|
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:
|
def grant_project_accesses(*, actor, workspace: Workspace, target_user, project_ids: list[str]) -> int:
|
||||||
ensure_workspace_project_access(actor, workspace)
|
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))
|
projects = list(Project.objects.filter(workspace=workspace, id__in=project_ids, is_deleted=False))
|
||||||
if len(projects) != len(set(project_ids)):
|
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:
|
def revoke_project_accesses(*, actor, workspace: Workspace, target_user, project_ids: list[str]) -> int:
|
||||||
ensure_workspace_project_access(actor, workspace)
|
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(
|
accesses = list(
|
||||||
ProjectAccess.objects.filter(
|
ProjectAccess.objects.filter(
|
||||||
|
|||||||
@@ -191,3 +191,29 @@ class ProjectViewTests(APITestCase):
|
|||||||
|
|
||||||
self.assertEqual(response.status_code, 400)
|
self.assertEqual(response.status_code, 400)
|
||||||
self.assertIn("Grant project access", response.data["detail"])
|
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