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

@@ -1,8 +1,4 @@
from apps.notifications.services.membership_events import (
notify_project_membership_added,
notify_project_membership_deactivated,
notify_project_membership_removed,
notify_project_membership_role_changed,
notify_workspace_membership_added,
notify_workspace_membership_deactivated,
notify_workspace_membership_removed,
@@ -16,8 +12,4 @@ __all__ = [
"notify_workspace_membership_role_changed",
"notify_workspace_membership_deactivated",
"notify_workspace_membership_removed",
"notify_project_membership_added",
"notify_project_membership_role_changed",
"notify_project_membership_deactivated",
"notify_project_membership_removed",
]

View File

@@ -31,10 +31,6 @@ def _workspace_action_url(workspace) -> str:
return f"/workspaces/{workspace.id}"
def _project_action_url(project) -> str:
return "/projects"
def notify_workspace_membership_added(*, actor, recipient, workspace, role: str) -> None:
if _should_skip(actor, recipient):
return
@@ -148,124 +144,3 @@ def notify_workspace_membership_removed(*, actor, recipient, workspace, role: st
},
},
)
def notify_project_membership_added(*, actor, recipient, project, role: str) -> None:
if _should_skip(actor, recipient):
return
actor_display = _actor_name(actor)
role_label = _role_label(recipient.project_memberships.model.Role, role)
_notify_user(
recipient,
{
"type": "project_membership_added",
"title": "Added to project",
"message": f"{actor_display} added you to {project.name} as {role_label}.",
"level": "info",
"action_url": _project_action_url(project),
"entity_type": "project",
"entity_id": str(project.id),
"meta": {
"workspace_id": str(project.workspace_id),
"workspace_name": project.workspace.name,
"project_id": str(project.id),
"project_name": project.name,
"actor_id": str(actor.id),
"actor_name": actor_display,
"new_role": role,
},
},
)
def notify_project_membership_role_changed(
*, actor, recipient, project, previous_role: str, new_role: str
) -> None:
if _should_skip(actor, recipient) or previous_role == new_role:
return
actor_display = _actor_name(actor)
previous_role_label = _role_label(recipient.project_memberships.model.Role, previous_role)
new_role_label = _role_label(recipient.project_memberships.model.Role, new_role)
_notify_user(
recipient,
{
"type": "project_membership_role_changed",
"title": "Project role changed",
"message": (
f"{actor_display} changed your role in {project.name} "
f"from {previous_role_label} to {new_role_label}."
),
"level": "info",
"action_url": _project_action_url(project),
"entity_type": "project",
"entity_id": str(project.id),
"meta": {
"workspace_id": str(project.workspace_id),
"workspace_name": project.workspace.name,
"project_id": str(project.id),
"project_name": project.name,
"actor_id": str(actor.id),
"actor_name": actor_display,
"previous_role": previous_role,
"new_role": new_role,
},
},
)
def notify_project_membership_deactivated(*, actor, recipient, project, role: str) -> None:
if _should_skip(actor, recipient):
return
actor_display = _actor_name(actor)
_notify_user(
recipient,
{
"type": "project_membership_deactivated",
"title": "Project access deactivated",
"message": f"{actor_display} deactivated your access to {project.name}.",
"level": "warning",
"action_url": _project_action_url(project),
"entity_type": "project",
"entity_id": str(project.id),
"meta": {
"workspace_id": str(project.workspace_id),
"workspace_name": project.workspace.name,
"project_id": str(project.id),
"project_name": project.name,
"actor_id": str(actor.id),
"actor_name": actor_display,
"previous_role": role,
},
},
)
def notify_project_membership_removed(*, actor, recipient, project, role: str) -> None:
if _should_skip(actor, recipient):
return
actor_display = _actor_name(actor)
_notify_user(
recipient,
{
"type": "project_membership_removed",
"title": "Removed from project",
"message": f"{actor_display} removed you from {project.name}.",
"level": "warning",
"action_url": _project_action_url(project),
"entity_type": "project",
"entity_id": str(project.id),
"meta": {
"workspace_id": str(project.workspace_id),
"workspace_name": project.workspace.name,
"project_id": str(project.id),
"project_name": project.name,
"actor_id": str(actor.id),
"actor_name": actor_display,
"previous_role": role,
},
},
)

View File

@@ -4,7 +4,6 @@ from rest_framework.test import APIClient
from apps.notifications.services import store as services
from apps.notifications.services import RedisNotificationStore
from apps.notifications.tests.test_services import FakeRedis
from apps.projects.models import Project, ProjectMembership
from apps.users.models import User
from apps.workspaces.models import Workspace, WorkspaceMembership
@@ -158,140 +157,3 @@ def test_workspace_membership_update_skips_self_notifications(
assert response.status_code == 403
assert _notifications_for(owner) == []
def test_project_create_notifies_initial_members_not_creator(
fake_redis, api_client, owner, member
):
workspace = Workspace.objects.create(name="Engineering", description="", owner=owner)
api_client.force_authenticate(user=owner)
response = api_client.post(
"/api/projects/",
{
"workspace": str(workspace.id),
"name": "API",
"description": "",
"client": None,
"members": [
{"user_id": str(member.id), "role": ProjectMembership.Role.MEMBER}
],
},
format="json",
)
assert response.status_code == 201
assert _notifications_for(owner) == []
notifications = _notifications_for(member)
assert len(notifications) == 1
assert notifications[0]["type"] == "project_membership_added"
assert notifications[0]["meta"]["project_name"] == "API"
def test_project_update_full_sync_notifies_only_real_deltas(
fake_redis, api_client, owner, member, another_member, third_member, fourth_member
):
workspace = Workspace.objects.create(name="Build", description="", owner=owner)
project = Project.objects.create(workspace=workspace, name="Platform", 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.MEMBER,
is_active=True,
)
ProjectMembership.objects.create(
project=project,
user=another_member,
role=ProjectMembership.Role.MEMBER,
is_active=True,
)
ProjectMembership.objects.create(
project=project,
user=third_member,
role=ProjectMembership.Role.MEMBER,
is_active=True,
)
api_client.force_authenticate(user=owner)
response = api_client.patch(
f"/api/projects/{project.id}/",
{
"client": None,
"members": [
{"user_id": str(member.id), "role": ProjectMembership.Role.MANAGER},
{"user_id": str(third_member.id), "role": ProjectMembership.Role.MEMBER},
{"user_id": str(fourth_member.id), "role": ProjectMembership.Role.MEMBER},
],
},
format="json",
)
assert response.status_code == 200
assert [item["type"] for item in _notifications_for(member)] == [
"project_membership_role_changed"
]
assert [item["type"] for item in _notifications_for(another_member)] == [
"project_membership_deactivated"
]
assert _notifications_for(third_member) == []
assert [item["type"] for item in _notifications_for(fourth_member)] == [
"project_membership_added"
]
assert _notifications_for(owner) == []
def test_project_membership_crud_emits_add_role_change_deactivate_and_remove(
fake_redis, api_client, owner, member
):
workspace = Workspace.objects.create(name="Data", description="", owner=owner)
project = Project.objects.create(workspace=workspace, name="Warehouse", description="")
ProjectMembership.objects.create(
project=project,
user=owner,
role=ProjectMembership.Role.MANAGER,
is_active=True,
)
api_client.force_authenticate(user=owner)
create_response = api_client.post(
"/api/memberships/",
{
"project_id": str(project.id),
"user_id": str(member.id),
"role": ProjectMembership.Role.MEMBER,
},
format="json",
)
assert create_response.status_code == 201
membership_id = create_response.data["id"]
role_response = api_client.patch(
f"/api/memberships/{membership_id}/",
{"role": ProjectMembership.Role.MANAGER},
format="json",
)
assert role_response.status_code == 200
deactivate_response = api_client.patch(
f"/api/memberships/{membership_id}/",
{"is_active": False},
format="json",
)
assert deactivate_response.status_code == 200
remove_response = api_client.delete(f"/api/memberships/{membership_id}/")
assert remove_response.status_code == 204
notifications = _notifications_for(member)
assert [item["type"] for item in notifications] == [
"project_membership_removed",
"project_membership_deactivated",
"project_membership_role_changed",
"project_membership_added",
]