refactor(projects): remove project membership access model

This commit is contained in:
2026-04-28 19:35:24 +03:30
parent 71924ce6fb
commit 1cd948592c
20 changed files with 150 additions and 905 deletions

View File

@@ -132,15 +132,17 @@ class WorkspaceSerializer(BaseModelSerializer):
class WorkspaceMembershipSerializer(BaseModelSerializer):
user = serializers.SerializerMethodField()
user_id = serializers.UUIDField(write_only=True, required=False)
class Meta:
model = WorkspaceMembership
fields = BaseModelSerializer.Meta.fields + (
"workspace",
"user",
"role",
"is_active",
)
"user_id",
"role",
"is_active",
)
def get_user(self, instance):
request = self.context.get("request")

View File

@@ -164,7 +164,11 @@ class WorkspaceMembershipViewSet(ModelViewSet):
status=status.HTTP_403_FORBIDDEN,
)
serializer = self.get_serializer(data=request.data)
payload = request.data.copy()
if payload.get("user") and not payload.get("user_id"):
payload["user_id"] = payload.get("user")
serializer = self.get_serializer(data=payload)
serializer.is_valid(raise_exception=True)
membership = serializer.save()
notify_workspace_membership_added(

View File

@@ -8,10 +8,6 @@ from apps.workspaces.services.permissions import (
PROJECTS_DELETE,
PROJECTS_EDIT,
PROJECTS_VIEW,
PROJECT_MEMBERS_ADD,
PROJECT_MEMBERS_CHANGE_ROLE,
PROJECT_MEMBERS_REMOVE,
PROJECT_MEMBERS_VIEW,
TAGS_CREATE,
TAGS_DELETE,
TAGS_EDIT,
@@ -62,10 +58,6 @@ __all__ = [
"PROJECTS_EDIT",
"PROJECTS_DELETE",
"PROJECTS_ARCHIVE",
"PROJECT_MEMBERS_VIEW",
"PROJECT_MEMBERS_ADD",
"PROJECT_MEMBERS_REMOVE",
"PROJECT_MEMBERS_CHANGE_ROLE",
"TIME_ENTRIES_VIEW_OWN",
"TIME_ENTRIES_MANAGE_OWN",
"get_workspace_membership",

View File

@@ -1,6 +1,5 @@
from __future__ import annotations
from apps.projects.models import ProjectMembership
from apps.workspaces.models import Workspace, WorkspaceMembership
@@ -25,22 +24,9 @@ PROJECTS_CREATE = "projects.create"
PROJECTS_EDIT = "projects.edit"
PROJECTS_DELETE = "projects.delete"
PROJECTS_ARCHIVE = "projects.archive"
PROJECT_MEMBERS_VIEW = "project_members.view"
PROJECT_MEMBERS_ADD = "project_members.add"
PROJECT_MEMBERS_REMOVE = "project_members.remove"
PROJECT_MEMBERS_CHANGE_ROLE = "project_members.change_role"
TIME_ENTRIES_VIEW_OWN = "time_entries.view_own"
TIME_ENTRIES_MANAGE_OWN = "time_entries.manage_own"
PROJECT_MANAGER_CAPABILITIES = {
PROJECTS_EDIT,
PROJECTS_ARCHIVE,
PROJECT_MEMBERS_VIEW,
PROJECT_MEMBERS_ADD,
PROJECT_MEMBERS_REMOVE,
PROJECT_MEMBERS_CHANGE_ROLE,
}
WORKSPACE_ROLE_CAPABILITIES = {
WorkspaceMembership.Role.OWNER: {
WORKSPACE_VIEW,
@@ -64,10 +50,6 @@ WORKSPACE_ROLE_CAPABILITIES = {
PROJECTS_EDIT,
PROJECTS_DELETE,
PROJECTS_ARCHIVE,
PROJECT_MEMBERS_VIEW,
PROJECT_MEMBERS_ADD,
PROJECT_MEMBERS_REMOVE,
PROJECT_MEMBERS_CHANGE_ROLE,
TIME_ENTRIES_VIEW_OWN,
TIME_ENTRIES_MANAGE_OWN,
},
@@ -92,10 +74,6 @@ WORKSPACE_ROLE_CAPABILITIES = {
PROJECTS_EDIT,
PROJECTS_DELETE,
PROJECTS_ARCHIVE,
PROJECT_MEMBERS_VIEW,
PROJECT_MEMBERS_ADD,
PROJECT_MEMBERS_REMOVE,
PROJECT_MEMBERS_CHANGE_ROLE,
TIME_ENTRIES_VIEW_OWN,
TIME_ENTRIES_MANAGE_OWN,
},
@@ -149,24 +127,7 @@ def has_workspace_capability(user, workspace: Workspace, capability: str) -> boo
def has_project_capability(user, project, capability: str) -> bool:
if has_workspace_capability(user, project.workspace, capability):
return True
workspace_role = get_workspace_role(user, project.workspace)
if workspace_role not in {
WorkspaceMembership.Role.OWNER,
WorkspaceMembership.Role.ADMIN,
}:
return False
is_project_manager = ProjectMembership.objects.filter(
project=project,
user=user,
role=ProjectMembership.Role.MANAGER,
is_active=True,
is_deleted=False,
).exists()
return is_project_manager and capability in PROJECT_MANAGER_CAPABILITIES
return has_workspace_capability(user, project.workspace, capability)
def can_delete_workspace_object(user, obj, capability: str) -> bool:

View File

@@ -5,7 +5,7 @@ from django.utils import timezone
from rest_framework.test import APIClient
from apps.clients.models import Client
from apps.projects.models import Project, ProjectMembership
from apps.projects.models import Project
from apps.tags.models import Tag
from apps.users.models import User
from apps.workspaces.models import Workspace, WorkspaceMembership
@@ -75,20 +75,7 @@ def workspace(owner, admin, member, guest):
@pytest.fixture()
def project(workspace, owner, member):
project = Project.objects.create(workspace=workspace, name="Alpha", description="")
ProjectMembership.objects.create(
project=project,
user=owner,
role=ProjectMembership.Role.MANAGER,
is_active=True,
)
ProjectMembership.objects.create(
project=project,
user=member,
role=ProjectMembership.Role.MANAGER,
is_active=True,
)
return project
return Project.objects.create(workspace=workspace, name="Alpha", description="")
def test_member_is_read_only_for_clients_and_projects(api_client, member, workspace, project):
@@ -118,16 +105,6 @@ def test_member_is_read_only_for_clients_and_projects(api_client, member, worksp
)
archive_project_response = api_client.post(f"/api/projects/{project.id}/archive/")
delete_project_response = api_client.delete(f"/api/projects/{project.id}/")
membership_response = api_client.post(
"/api/memberships/",
{
"project_id": str(project.id),
"user_id": str(workspace.owner_id),
"role": ProjectMembership.Role.MEMBER,
},
format="json",
)
assert client_response.status_code == 403
assert update_client_response.status_code == 403
assert delete_client_response.status_code == 403
@@ -135,7 +112,6 @@ def test_member_is_read_only_for_clients_and_projects(api_client, member, worksp
assert update_project_response.status_code == 403
assert archive_project_response.status_code == 403
assert delete_project_response.status_code == 403
assert membership_response.status_code == 403
def test_member_can_create_tags_and_manage_own_time_entries(api_client, owner, member, workspace):
@@ -218,7 +194,7 @@ def test_guest_is_read_only_for_workspace_resources(api_client, owner, guest, wo
assert edit_project_response.status_code == 403
def test_member_project_manager_cannot_edit_project(api_client, member, project):
def test_member_cannot_edit_project(api_client, member, project):
api_client.force_authenticate(user=member)
response = api_client.patch(

View File

@@ -3,7 +3,7 @@ from decimal import Decimal
import pytest
from rest_framework.test import APIClient
from apps.projects.models import Project, ProjectMembership
from apps.projects.models import Project
from apps.time_entries.services.rates import resolve_rate
from apps.users.models import User
from apps.workspaces.models import PriceUnit, Workspace, WorkspaceMembership, WorkspaceUserRate
@@ -39,11 +39,7 @@ def workspace(owner, admin, member):
@pytest.fixture()
def project(workspace, owner, admin, member):
project = Project.objects.create(workspace=workspace, name="Billing")
ProjectMembership.objects.create(project=project, user=owner, role=ProjectMembership.Role.MANAGER, is_active=True)
ProjectMembership.objects.create(project=project, user=admin, role=ProjectMembership.Role.MANAGER, is_active=True)
ProjectMembership.objects.create(project=project, user=member, role=ProjectMembership.Role.MEMBER, is_active=True)
return project
return Project.objects.create(workspace=workspace, name="Billing")
@pytest.fixture()