forked from github/plane
chore: user workflow (#2762)
* dev: workspace member deactivation and leave endpoints and filters * dev: deactivated for project members * dev: project members leave * dev: project member check on workspace deactivation * dev: project member queryset update and remove leave project endpoint * dev: rename is_deactivated to is_active and user deactivation apis * dev: check if the user is already part of workspace then make them active * dev: workspace and project save * dev: update project members to make them active * dev: project invitation * dev: automatic user workspace and project member create when user sign in/up * dev: fix member invites * dev: rename deactivation variable * dev: update project member invitation * dev: additional permission layer for workspace * dev: update the url for workspace invitations * dev: remove invitation urls from users * dev: cleanup workspace invitation workflow * dev: workspace and project invitation
This commit is contained in:
parent
96f6e37cc5
commit
fc51ffc589
@ -1,2 +1,2 @@
|
|||||||
from .workspace import WorkSpaceBasePermission, WorkSpaceAdminPermission, WorkspaceEntityPermission, WorkspaceViewerPermission
|
from .workspace import WorkSpaceBasePermission, WorkSpaceAdminPermission, WorkspaceEntityPermission, WorkspaceViewerPermission, WorkspaceUserPermission
|
||||||
from .project import ProjectBasePermission, ProjectEntityPermission, ProjectMemberPermission, ProjectLitePermission
|
from .project import ProjectBasePermission, ProjectEntityPermission, ProjectMemberPermission, ProjectLitePermission
|
||||||
|
@ -13,14 +13,15 @@ Guest = 5
|
|||||||
|
|
||||||
class ProjectBasePermission(BasePermission):
|
class ProjectBasePermission(BasePermission):
|
||||||
def has_permission(self, request, view):
|
def has_permission(self, request, view):
|
||||||
|
|
||||||
if request.user.is_anonymous:
|
if request.user.is_anonymous:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
## Safe Methods -> Handle the filtering logic in queryset
|
## Safe Methods -> Handle the filtering logic in queryset
|
||||||
if request.method in SAFE_METHODS:
|
if request.method in SAFE_METHODS:
|
||||||
return WorkspaceMember.objects.filter(
|
return WorkspaceMember.objects.filter(
|
||||||
workspace__slug=view.workspace_slug, member=request.user
|
workspace__slug=view.workspace_slug,
|
||||||
|
member=request.user,
|
||||||
|
is_active=True,
|
||||||
).exists()
|
).exists()
|
||||||
|
|
||||||
## Only workspace owners or admins can create the projects
|
## Only workspace owners or admins can create the projects
|
||||||
@ -29,6 +30,7 @@ class ProjectBasePermission(BasePermission):
|
|||||||
workspace__slug=view.workspace_slug,
|
workspace__slug=view.workspace_slug,
|
||||||
member=request.user,
|
member=request.user,
|
||||||
role__in=[Admin, Member],
|
role__in=[Admin, Member],
|
||||||
|
is_active=True,
|
||||||
).exists()
|
).exists()
|
||||||
|
|
||||||
## Only Project Admins can update project attributes
|
## Only Project Admins can update project attributes
|
||||||
@ -37,19 +39,21 @@ class ProjectBasePermission(BasePermission):
|
|||||||
member=request.user,
|
member=request.user,
|
||||||
role=Admin,
|
role=Admin,
|
||||||
project_id=view.project_id,
|
project_id=view.project_id,
|
||||||
|
is_active=True,
|
||||||
).exists()
|
).exists()
|
||||||
|
|
||||||
|
|
||||||
class ProjectMemberPermission(BasePermission):
|
class ProjectMemberPermission(BasePermission):
|
||||||
def has_permission(self, request, view):
|
def has_permission(self, request, view):
|
||||||
|
|
||||||
if request.user.is_anonymous:
|
if request.user.is_anonymous:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
## Safe Methods -> Handle the filtering logic in queryset
|
## Safe Methods -> Handle the filtering logic in queryset
|
||||||
if request.method in SAFE_METHODS:
|
if request.method in SAFE_METHODS:
|
||||||
return ProjectMember.objects.filter(
|
return ProjectMember.objects.filter(
|
||||||
workspace__slug=view.workspace_slug, member=request.user
|
workspace__slug=view.workspace_slug,
|
||||||
|
member=request.user,
|
||||||
|
is_active=True,
|
||||||
).exists()
|
).exists()
|
||||||
## Only workspace owners or admins can create the projects
|
## Only workspace owners or admins can create the projects
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
@ -57,6 +61,7 @@ class ProjectMemberPermission(BasePermission):
|
|||||||
workspace__slug=view.workspace_slug,
|
workspace__slug=view.workspace_slug,
|
||||||
member=request.user,
|
member=request.user,
|
||||||
role__in=[Admin, Member],
|
role__in=[Admin, Member],
|
||||||
|
is_active=True,
|
||||||
).exists()
|
).exists()
|
||||||
|
|
||||||
## Only Project Admins can update project attributes
|
## Only Project Admins can update project attributes
|
||||||
@ -65,12 +70,12 @@ class ProjectMemberPermission(BasePermission):
|
|||||||
member=request.user,
|
member=request.user,
|
||||||
role__in=[Admin, Member],
|
role__in=[Admin, Member],
|
||||||
project_id=view.project_id,
|
project_id=view.project_id,
|
||||||
|
is_active=True,
|
||||||
).exists()
|
).exists()
|
||||||
|
|
||||||
|
|
||||||
class ProjectEntityPermission(BasePermission):
|
class ProjectEntityPermission(BasePermission):
|
||||||
def has_permission(self, request, view):
|
def has_permission(self, request, view):
|
||||||
|
|
||||||
if request.user.is_anonymous:
|
if request.user.is_anonymous:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@ -80,6 +85,7 @@ class ProjectEntityPermission(BasePermission):
|
|||||||
workspace__slug=view.workspace_slug,
|
workspace__slug=view.workspace_slug,
|
||||||
member=request.user,
|
member=request.user,
|
||||||
project_id=view.project_id,
|
project_id=view.project_id,
|
||||||
|
is_active=True,
|
||||||
).exists()
|
).exists()
|
||||||
|
|
||||||
## Only project members or admins can create and edit the project attributes
|
## Only project members or admins can create and edit the project attributes
|
||||||
@ -88,17 +94,18 @@ class ProjectEntityPermission(BasePermission):
|
|||||||
member=request.user,
|
member=request.user,
|
||||||
role__in=[Admin, Member],
|
role__in=[Admin, Member],
|
||||||
project_id=view.project_id,
|
project_id=view.project_id,
|
||||||
|
is_active=True,
|
||||||
).exists()
|
).exists()
|
||||||
|
|
||||||
|
|
||||||
class ProjectLitePermission(BasePermission):
|
class ProjectLitePermission(BasePermission):
|
||||||
|
|
||||||
def has_permission(self, request, view):
|
def has_permission(self, request, view):
|
||||||
if request.user.is_anonymous:
|
if request.user.is_anonymous:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
return ProjectMember.objects.filter(
|
return ProjectMember.objects.filter(
|
||||||
workspace__slug=view.workspace_slug,
|
workspace__slug=view.workspace_slug,
|
||||||
member=request.user,
|
member=request.user,
|
||||||
project_id=view.project_id,
|
project_id=view.project_id,
|
||||||
|
is_active=True,
|
||||||
).exists()
|
).exists()
|
||||||
|
@ -32,12 +32,16 @@ class WorkSpaceBasePermission(BasePermission):
|
|||||||
member=request.user,
|
member=request.user,
|
||||||
workspace__slug=view.workspace_slug,
|
workspace__slug=view.workspace_slug,
|
||||||
role__in=[Owner, Admin],
|
role__in=[Owner, Admin],
|
||||||
|
is_active=True,
|
||||||
).exists()
|
).exists()
|
||||||
|
|
||||||
# allow only owner to delete the workspace
|
# allow only owner to delete the workspace
|
||||||
if request.method == "DELETE":
|
if request.method == "DELETE":
|
||||||
return WorkspaceMember.objects.filter(
|
return WorkspaceMember.objects.filter(
|
||||||
member=request.user, workspace__slug=view.workspace_slug, role=Owner
|
member=request.user,
|
||||||
|
workspace__slug=view.workspace_slug,
|
||||||
|
role=Owner,
|
||||||
|
is_active=True,
|
||||||
).exists()
|
).exists()
|
||||||
|
|
||||||
|
|
||||||
@ -50,6 +54,7 @@ class WorkSpaceAdminPermission(BasePermission):
|
|||||||
member=request.user,
|
member=request.user,
|
||||||
workspace__slug=view.workspace_slug,
|
workspace__slug=view.workspace_slug,
|
||||||
role__in=[Owner, Admin],
|
role__in=[Owner, Admin],
|
||||||
|
is_active=True,
|
||||||
).exists()
|
).exists()
|
||||||
|
|
||||||
|
|
||||||
@ -63,12 +68,14 @@ class WorkspaceEntityPermission(BasePermission):
|
|||||||
return WorkspaceMember.objects.filter(
|
return WorkspaceMember.objects.filter(
|
||||||
workspace__slug=view.workspace_slug,
|
workspace__slug=view.workspace_slug,
|
||||||
member=request.user,
|
member=request.user,
|
||||||
|
is_active=True,
|
||||||
).exists()
|
).exists()
|
||||||
|
|
||||||
return WorkspaceMember.objects.filter(
|
return WorkspaceMember.objects.filter(
|
||||||
member=request.user,
|
member=request.user,
|
||||||
workspace__slug=view.workspace_slug,
|
workspace__slug=view.workspace_slug,
|
||||||
role__in=[Owner, Admin],
|
role__in=[Owner, Admin],
|
||||||
|
is_active=True,
|
||||||
).exists()
|
).exists()
|
||||||
|
|
||||||
|
|
||||||
@ -78,5 +85,18 @@ class WorkspaceViewerPermission(BasePermission):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
return WorkspaceMember.objects.filter(
|
return WorkspaceMember.objects.filter(
|
||||||
member=request.user, workspace__slug=view.workspace_slug, role__gte=10
|
member=request.user,
|
||||||
|
workspace__slug=view.workspace_slug,
|
||||||
|
role__gte=10,
|
||||||
|
is_active=True,
|
||||||
).exists()
|
).exists()
|
||||||
|
|
||||||
|
|
||||||
|
class WorkspaceUserPermission(BasePermission):
|
||||||
|
|
||||||
|
def has_permission(self, request, view):
|
||||||
|
return WorkspaceMember.objects.filter(
|
||||||
|
member=request.user,
|
||||||
|
workspace__slug=view.workspace_slug,
|
||||||
|
is_active=True,
|
||||||
|
)
|
||||||
|
@ -103,7 +103,10 @@ class ProjectListSerializer(DynamicBaseSerializer):
|
|||||||
members = serializers.SerializerMethodField()
|
members = serializers.SerializerMethodField()
|
||||||
|
|
||||||
def get_members(self, obj):
|
def get_members(self, obj):
|
||||||
project_members = ProjectMember.objects.filter(project_id=obj.id).values(
|
project_members = ProjectMember.objects.filter(
|
||||||
|
project_id=obj.id,
|
||||||
|
is_active=True,
|
||||||
|
).values(
|
||||||
"id",
|
"id",
|
||||||
"member_id",
|
"member_id",
|
||||||
"member__display_name",
|
"member__display_name",
|
||||||
|
@ -2,17 +2,16 @@ from django.urls import path
|
|||||||
|
|
||||||
from plane.api.views import (
|
from plane.api.views import (
|
||||||
ProjectViewSet,
|
ProjectViewSet,
|
||||||
InviteProjectEndpoint,
|
ProjectInvitationsViewset,
|
||||||
ProjectMemberViewSet,
|
ProjectMemberViewSet,
|
||||||
ProjectMemberInvitationsViewset,
|
|
||||||
ProjectMemberUserEndpoint,
|
ProjectMemberUserEndpoint,
|
||||||
ProjectJoinEndpoint,
|
ProjectJoinEndpoint,
|
||||||
AddTeamToProjectEndpoint,
|
AddTeamToProjectEndpoint,
|
||||||
ProjectUserViewsEndpoint,
|
ProjectUserViewsEndpoint,
|
||||||
ProjectIdentifierEndpoint,
|
ProjectIdentifierEndpoint,
|
||||||
ProjectFavoritesViewSet,
|
ProjectFavoritesViewSet,
|
||||||
LeaveProjectEndpoint,
|
|
||||||
ProjectPublicCoverImagesEndpoint,
|
ProjectPublicCoverImagesEndpoint,
|
||||||
|
UserProjectInvitationsViewset,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -45,13 +44,48 @@ urlpatterns = [
|
|||||||
name="project-identifiers",
|
name="project-identifiers",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"workspaces/<str:slug>/projects/<uuid:project_id>/invite/",
|
"workspaces/<str:slug>/projects/<uuid:project_id>/invitations/",
|
||||||
InviteProjectEndpoint.as_view(),
|
ProjectInvitationsViewset.as_view(
|
||||||
name="invite-project",
|
{
|
||||||
|
"get": "list",
|
||||||
|
"post": "create",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
name="project-member-invite",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/<uuid:project_id>/invitations/<uuid:pk>/",
|
||||||
|
ProjectInvitationsViewset.as_view(
|
||||||
|
{
|
||||||
|
"get": "retrieve",
|
||||||
|
"delete": "destroy",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="project-member-invite",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"users/me/invitations/projects/",
|
||||||
|
UserProjectInvitationsViewset.as_view(
|
||||||
|
{
|
||||||
|
"get": "list",
|
||||||
|
"post": "create",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
name="user-project-invitations",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/projects/join/",
|
||||||
|
ProjectJoinEndpoint.as_view(),
|
||||||
|
name="project-join",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"workspaces/<str:slug>/projects/<uuid:project_id>/members/",
|
"workspaces/<str:slug>/projects/<uuid:project_id>/members/",
|
||||||
ProjectMemberViewSet.as_view({"get": "list", "post": "create"}),
|
ProjectMemberViewSet.as_view(
|
||||||
|
{
|
||||||
|
"get": "list",
|
||||||
|
"post": "create",
|
||||||
|
}
|
||||||
|
),
|
||||||
name="project-member",
|
name="project-member",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
@ -66,30 +100,19 @@ urlpatterns = [
|
|||||||
name="project-member",
|
name="project-member",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"workspaces/<str:slug>/projects/join/",
|
"workspaces/<str:slug>/projects/<uuid:project_id>/members/leave/",
|
||||||
ProjectJoinEndpoint.as_view(),
|
ProjectMemberViewSet.as_view(
|
||||||
name="project-join",
|
{
|
||||||
|
"post": "leave",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
name="project-member",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"workspaces/<str:slug>/projects/<uuid:project_id>/team-invite/",
|
"workspaces/<str:slug>/projects/<uuid:project_id>/team-invite/",
|
||||||
AddTeamToProjectEndpoint.as_view(),
|
AddTeamToProjectEndpoint.as_view(),
|
||||||
name="projects",
|
name="projects",
|
||||||
),
|
),
|
||||||
path(
|
|
||||||
"workspaces/<str:slug>/projects/<uuid:project_id>/invitations/",
|
|
||||||
ProjectMemberInvitationsViewset.as_view({"get": "list"}),
|
|
||||||
name="project-member-invite",
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
"workspaces/<str:slug>/projects/<uuid:project_id>/invitations/<uuid:pk>/",
|
|
||||||
ProjectMemberInvitationsViewset.as_view(
|
|
||||||
{
|
|
||||||
"get": "retrieve",
|
|
||||||
"delete": "destroy",
|
|
||||||
}
|
|
||||||
),
|
|
||||||
name="project-member-invite",
|
|
||||||
),
|
|
||||||
path(
|
path(
|
||||||
"workspaces/<str:slug>/projects/<uuid:project_id>/project-views/",
|
"workspaces/<str:slug>/projects/<uuid:project_id>/project-views/",
|
||||||
ProjectUserViewsEndpoint.as_view(),
|
ProjectUserViewsEndpoint.as_view(),
|
||||||
@ -119,11 +142,6 @@ urlpatterns = [
|
|||||||
),
|
),
|
||||||
name="project-favorite",
|
name="project-favorite",
|
||||||
),
|
),
|
||||||
path(
|
|
||||||
"workspaces/<str:slug>/projects/<uuid:project_id>/members/leave/",
|
|
||||||
LeaveProjectEndpoint.as_view(),
|
|
||||||
name="leave-project",
|
|
||||||
),
|
|
||||||
path(
|
path(
|
||||||
"project-covers/",
|
"project-covers/",
|
||||||
ProjectPublicCoverImagesEndpoint.as_view(),
|
ProjectPublicCoverImagesEndpoint.as_view(),
|
||||||
|
@ -9,15 +9,10 @@ from plane.api.views import (
|
|||||||
ChangePasswordEndpoint,
|
ChangePasswordEndpoint,
|
||||||
## End User
|
## End User
|
||||||
## Workspaces
|
## Workspaces
|
||||||
UserWorkspaceInvitationsEndpoint,
|
|
||||||
UserWorkSpacesEndpoint,
|
UserWorkSpacesEndpoint,
|
||||||
JoinWorkspaceEndpoint,
|
|
||||||
UserWorkspaceInvitationsEndpoint,
|
|
||||||
UserWorkspaceInvitationEndpoint,
|
|
||||||
UserActivityGraphEndpoint,
|
UserActivityGraphEndpoint,
|
||||||
UserIssueCompletedGraphEndpoint,
|
UserIssueCompletedGraphEndpoint,
|
||||||
UserWorkspaceDashboardEndpoint,
|
UserWorkspaceDashboardEndpoint,
|
||||||
UserProjectInvitationsViewset,
|
|
||||||
## End Workspaces
|
## End Workspaces
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -26,7 +21,11 @@ urlpatterns = [
|
|||||||
path(
|
path(
|
||||||
"users/me/",
|
"users/me/",
|
||||||
UserEndpoint.as_view(
|
UserEndpoint.as_view(
|
||||||
{"get": "retrieve", "patch": "partial_update", "delete": "destroy"}
|
{
|
||||||
|
"get": "retrieve",
|
||||||
|
"patch": "partial_update",
|
||||||
|
"delete": "deactivate",
|
||||||
|
}
|
||||||
),
|
),
|
||||||
name="users",
|
name="users",
|
||||||
),
|
),
|
||||||
@ -65,23 +64,6 @@ urlpatterns = [
|
|||||||
UserWorkSpacesEndpoint.as_view(),
|
UserWorkSpacesEndpoint.as_view(),
|
||||||
name="user-workspace",
|
name="user-workspace",
|
||||||
),
|
),
|
||||||
# user workspace invitations
|
|
||||||
path(
|
|
||||||
"users/me/invitations/workspaces/",
|
|
||||||
UserWorkspaceInvitationsEndpoint.as_view({"get": "list", "post": "create"}),
|
|
||||||
name="user-workspace-invitations",
|
|
||||||
),
|
|
||||||
# user workspace invitation
|
|
||||||
path(
|
|
||||||
"users/me/invitations/<uuid:pk>/",
|
|
||||||
UserWorkspaceInvitationEndpoint.as_view(
|
|
||||||
{
|
|
||||||
"get": "retrieve",
|
|
||||||
}
|
|
||||||
),
|
|
||||||
name="user-workspace-invitation",
|
|
||||||
),
|
|
||||||
# user join workspace
|
|
||||||
# User Graphs
|
# User Graphs
|
||||||
path(
|
path(
|
||||||
"users/me/workspaces/<str:slug>/activity-graph/",
|
"users/me/workspaces/<str:slug>/activity-graph/",
|
||||||
@ -99,15 +81,4 @@ urlpatterns = [
|
|||||||
name="user-workspace-dashboard",
|
name="user-workspace-dashboard",
|
||||||
),
|
),
|
||||||
## End User Graph
|
## End User Graph
|
||||||
path(
|
|
||||||
"users/me/invitations/workspaces/<str:slug>/<uuid:pk>/join/",
|
|
||||||
JoinWorkspaceEndpoint.as_view(),
|
|
||||||
name="user-join-workspace",
|
|
||||||
),
|
|
||||||
# user project invitations
|
|
||||||
path(
|
|
||||||
"users/me/invitations/projects/",
|
|
||||||
UserProjectInvitationsViewset.as_view({"get": "list", "post": "create"}),
|
|
||||||
name="user-project-invitations",
|
|
||||||
),
|
|
||||||
]
|
]
|
||||||
|
@ -2,8 +2,9 @@ from django.urls import path
|
|||||||
|
|
||||||
|
|
||||||
from plane.api.views import (
|
from plane.api.views import (
|
||||||
|
UserWorkspaceInvitationsViewSet,
|
||||||
WorkSpaceViewSet,
|
WorkSpaceViewSet,
|
||||||
InviteWorkspaceEndpoint,
|
WorkspaceJoinEndpoint,
|
||||||
WorkSpaceMemberViewSet,
|
WorkSpaceMemberViewSet,
|
||||||
WorkspaceInvitationsViewset,
|
WorkspaceInvitationsViewset,
|
||||||
WorkspaceMemberUserEndpoint,
|
WorkspaceMemberUserEndpoint,
|
||||||
@ -17,7 +18,6 @@ from plane.api.views import (
|
|||||||
WorkspaceUserProfileEndpoint,
|
WorkspaceUserProfileEndpoint,
|
||||||
WorkspaceUserProfileIssuesEndpoint,
|
WorkspaceUserProfileIssuesEndpoint,
|
||||||
WorkspaceLabelsEndpoint,
|
WorkspaceLabelsEndpoint,
|
||||||
LeaveWorkspaceEndpoint,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -49,14 +49,14 @@ urlpatterns = [
|
|||||||
),
|
),
|
||||||
name="workspace",
|
name="workspace",
|
||||||
),
|
),
|
||||||
path(
|
|
||||||
"workspaces/<str:slug>/invite/",
|
|
||||||
InviteWorkspaceEndpoint.as_view(),
|
|
||||||
name="invite-workspace",
|
|
||||||
),
|
|
||||||
path(
|
path(
|
||||||
"workspaces/<str:slug>/invitations/",
|
"workspaces/<str:slug>/invitations/",
|
||||||
WorkspaceInvitationsViewset.as_view({"get": "list"}),
|
WorkspaceInvitationsViewset.as_view(
|
||||||
|
{
|
||||||
|
"get": "list",
|
||||||
|
"post": "create",
|
||||||
|
},
|
||||||
|
),
|
||||||
name="workspace-invitations",
|
name="workspace-invitations",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
@ -69,6 +69,23 @@ urlpatterns = [
|
|||||||
),
|
),
|
||||||
name="workspace-invitations",
|
name="workspace-invitations",
|
||||||
),
|
),
|
||||||
|
# user workspace invitations
|
||||||
|
path(
|
||||||
|
"users/me/workspaces/invitations/",
|
||||||
|
UserWorkspaceInvitationsViewSet.as_view(
|
||||||
|
{
|
||||||
|
"get": "list",
|
||||||
|
"post": "create",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
name="user-workspace-invitations",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/invitations/<uuid:pk>/join/",
|
||||||
|
WorkspaceJoinEndpoint.as_view(),
|
||||||
|
name="workspace-join",
|
||||||
|
),
|
||||||
|
# user join workspace
|
||||||
path(
|
path(
|
||||||
"workspaces/<str:slug>/members/",
|
"workspaces/<str:slug>/members/",
|
||||||
WorkSpaceMemberViewSet.as_view({"get": "list"}),
|
WorkSpaceMemberViewSet.as_view({"get": "list"}),
|
||||||
@ -85,6 +102,15 @@ urlpatterns = [
|
|||||||
),
|
),
|
||||||
name="workspace-member",
|
name="workspace-member",
|
||||||
),
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/members/leave/",
|
||||||
|
WorkSpaceMemberViewSet.as_view(
|
||||||
|
{
|
||||||
|
"post": "leave",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
name="leave-workspace-members",
|
||||||
|
),
|
||||||
path(
|
path(
|
||||||
"workspaces/<str:slug>/teams/",
|
"workspaces/<str:slug>/teams/",
|
||||||
TeamMemberViewSet.as_view(
|
TeamMemberViewSet.as_view(
|
||||||
@ -168,9 +194,4 @@ urlpatterns = [
|
|||||||
WorkspaceLabelsEndpoint.as_view(),
|
WorkspaceLabelsEndpoint.as_view(),
|
||||||
name="workspace-labels",
|
name="workspace-labels",
|
||||||
),
|
),
|
||||||
path(
|
|
||||||
"workspaces/<str:slug>/members/leave/",
|
|
||||||
LeaveWorkspaceEndpoint.as_view(),
|
|
||||||
name="leave-workspace-members",
|
|
||||||
),
|
|
||||||
]
|
]
|
||||||
|
@ -2,10 +2,8 @@ from .project import (
|
|||||||
ProjectViewSet,
|
ProjectViewSet,
|
||||||
ProjectMemberViewSet,
|
ProjectMemberViewSet,
|
||||||
UserProjectInvitationsViewset,
|
UserProjectInvitationsViewset,
|
||||||
InviteProjectEndpoint,
|
ProjectInvitationsViewset,
|
||||||
AddTeamToProjectEndpoint,
|
AddTeamToProjectEndpoint,
|
||||||
ProjectMemberInvitationsViewset,
|
|
||||||
ProjectMemberInviteDetailViewSet,
|
|
||||||
ProjectIdentifierEndpoint,
|
ProjectIdentifierEndpoint,
|
||||||
ProjectJoinEndpoint,
|
ProjectJoinEndpoint,
|
||||||
ProjectUserViewsEndpoint,
|
ProjectUserViewsEndpoint,
|
||||||
@ -14,7 +12,6 @@ from .project import (
|
|||||||
ProjectDeployBoardViewSet,
|
ProjectDeployBoardViewSet,
|
||||||
ProjectDeployBoardPublicSettingsEndpoint,
|
ProjectDeployBoardPublicSettingsEndpoint,
|
||||||
WorkspaceProjectDeployBoardEndpoint,
|
WorkspaceProjectDeployBoardEndpoint,
|
||||||
LeaveProjectEndpoint,
|
|
||||||
ProjectPublicCoverImagesEndpoint,
|
ProjectPublicCoverImagesEndpoint,
|
||||||
)
|
)
|
||||||
from .user import (
|
from .user import (
|
||||||
@ -32,13 +29,11 @@ from .workspace import (
|
|||||||
WorkSpaceViewSet,
|
WorkSpaceViewSet,
|
||||||
UserWorkSpacesEndpoint,
|
UserWorkSpacesEndpoint,
|
||||||
WorkSpaceAvailabilityCheckEndpoint,
|
WorkSpaceAvailabilityCheckEndpoint,
|
||||||
InviteWorkspaceEndpoint,
|
WorkspaceJoinEndpoint,
|
||||||
JoinWorkspaceEndpoint,
|
|
||||||
WorkSpaceMemberViewSet,
|
WorkSpaceMemberViewSet,
|
||||||
TeamMemberViewSet,
|
TeamMemberViewSet,
|
||||||
WorkspaceInvitationsViewset,
|
WorkspaceInvitationsViewset,
|
||||||
UserWorkspaceInvitationsEndpoint,
|
UserWorkspaceInvitationsViewSet,
|
||||||
UserWorkspaceInvitationEndpoint,
|
|
||||||
UserLastProjectWithWorkspaceEndpoint,
|
UserLastProjectWithWorkspaceEndpoint,
|
||||||
WorkspaceMemberUserEndpoint,
|
WorkspaceMemberUserEndpoint,
|
||||||
WorkspaceMemberUserViewsEndpoint,
|
WorkspaceMemberUserViewsEndpoint,
|
||||||
@ -51,7 +46,6 @@ from .workspace import (
|
|||||||
WorkspaceUserProfileEndpoint,
|
WorkspaceUserProfileEndpoint,
|
||||||
WorkspaceUserProfileIssuesEndpoint,
|
WorkspaceUserProfileIssuesEndpoint,
|
||||||
WorkspaceLabelsEndpoint,
|
WorkspaceLabelsEndpoint,
|
||||||
LeaveWorkspaceEndpoint,
|
|
||||||
)
|
)
|
||||||
from .state import StateViewSet
|
from .state import StateViewSet
|
||||||
from .view import (
|
from .view import (
|
||||||
|
@ -4,7 +4,7 @@ import random
|
|||||||
import string
|
import string
|
||||||
import json
|
import json
|
||||||
import requests
|
import requests
|
||||||
|
from requests.exceptions import RequestException
|
||||||
# Django imports
|
# Django imports
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
@ -22,8 +22,13 @@ from sentry_sdk import capture_exception, capture_message
|
|||||||
|
|
||||||
# Module imports
|
# Module imports
|
||||||
from . import BaseAPIView
|
from . import BaseAPIView
|
||||||
from plane.db.models import User
|
from plane.db.models import (
|
||||||
from plane.api.serializers import UserSerializer
|
User,
|
||||||
|
WorkspaceMemberInvite,
|
||||||
|
WorkspaceMember,
|
||||||
|
ProjectMemberInvite,
|
||||||
|
ProjectMember,
|
||||||
|
)
|
||||||
from plane.settings.redis import redis_instance
|
from plane.settings.redis import redis_instance
|
||||||
from plane.bgtasks.magic_link_code_task import magic_link
|
from plane.bgtasks.magic_link_code_task import magic_link
|
||||||
|
|
||||||
@ -86,35 +91,93 @@ class SignUpEndpoint(BaseAPIView):
|
|||||||
user.token_updated_at = timezone.now()
|
user.token_updated_at = timezone.now()
|
||||||
user.save()
|
user.save()
|
||||||
|
|
||||||
|
# Check if user has any accepted invites for workspace and add them to workspace
|
||||||
|
workspace_member_invites = WorkspaceMemberInvite.objects.filter(
|
||||||
|
email=user.email, accepted=True
|
||||||
|
)
|
||||||
|
|
||||||
|
WorkspaceMember.objects.bulk_create(
|
||||||
|
[
|
||||||
|
WorkspaceMember(
|
||||||
|
workspace_id=workspace_member_invite.workspace_id,
|
||||||
|
member=user,
|
||||||
|
role=workspace_member_invite.role,
|
||||||
|
)
|
||||||
|
for workspace_member_invite in workspace_member_invites
|
||||||
|
],
|
||||||
|
ignore_conflicts=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check if user has any project invites
|
||||||
|
project_member_invites = ProjectMemberInvite.objects.filter(
|
||||||
|
email=user.email, accepted=True
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add user to workspace
|
||||||
|
WorkspaceMember.objects.bulk_create(
|
||||||
|
[
|
||||||
|
WorkspaceMember(
|
||||||
|
workspace_id=project_member_invite.workspace_id,
|
||||||
|
role=project_member_invite.role
|
||||||
|
if project_member_invite.role in [5, 10, 15]
|
||||||
|
else 15,
|
||||||
|
member=user,
|
||||||
|
created_by_id=project_member_invite.created_by_id,
|
||||||
|
)
|
||||||
|
for project_member_invite in project_member_invites
|
||||||
|
],
|
||||||
|
ignore_conflicts=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Now add the users to project
|
||||||
|
ProjectMember.objects.bulk_create(
|
||||||
|
[
|
||||||
|
ProjectMember(
|
||||||
|
workspace_id=project_member_invite.workspace_id,
|
||||||
|
role=project_member_invite.role
|
||||||
|
if project_member_invite.role in [5, 10, 15]
|
||||||
|
else 15,
|
||||||
|
member=user,
|
||||||
|
created_by_id=project_member_invite.created_by_id,
|
||||||
|
) for project_member_invite in project_member_invites
|
||||||
|
],
|
||||||
|
ignore_conflicts=True,
|
||||||
|
)
|
||||||
|
# Delete all the invites
|
||||||
|
workspace_member_invites.delete()
|
||||||
|
project_member_invites.delete()
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Send Analytics
|
||||||
|
if settings.ANALYTICS_BASE_API:
|
||||||
|
_ = requests.post(
|
||||||
|
settings.ANALYTICS_BASE_API,
|
||||||
|
headers={
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"X-Auth-Token": settings.ANALYTICS_SECRET_KEY,
|
||||||
|
},
|
||||||
|
json={
|
||||||
|
"event_id": uuid.uuid4().hex,
|
||||||
|
"event_data": {
|
||||||
|
"medium": "email",
|
||||||
|
},
|
||||||
|
"user": {"email": email, "id": str(user.id)},
|
||||||
|
"device_ctx": {
|
||||||
|
"ip": request.META.get("REMOTE_ADDR"),
|
||||||
|
"user_agent": request.META.get("HTTP_USER_AGENT"),
|
||||||
|
},
|
||||||
|
"event_type": "SIGN_UP",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
except RequestException as e:
|
||||||
|
capture_exception(e)
|
||||||
|
|
||||||
access_token, refresh_token = get_tokens_for_user(user)
|
access_token, refresh_token = get_tokens_for_user(user)
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
"access_token": access_token,
|
"access_token": access_token,
|
||||||
"refresh_token": refresh_token,
|
"refresh_token": refresh_token,
|
||||||
}
|
}
|
||||||
|
|
||||||
# Send Analytics
|
|
||||||
if settings.ANALYTICS_BASE_API:
|
|
||||||
_ = requests.post(
|
|
||||||
settings.ANALYTICS_BASE_API,
|
|
||||||
headers={
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
"X-Auth-Token": settings.ANALYTICS_SECRET_KEY,
|
|
||||||
},
|
|
||||||
json={
|
|
||||||
"event_id": uuid.uuid4().hex,
|
|
||||||
"event_data": {
|
|
||||||
"medium": "email",
|
|
||||||
},
|
|
||||||
"user": {"email": email, "id": str(user.id)},
|
|
||||||
"device_ctx": {
|
|
||||||
"ip": request.META.get("REMOTE_ADDR"),
|
|
||||||
"user_agent": request.META.get("HTTP_USER_AGENT"),
|
|
||||||
},
|
|
||||||
"event_type": "SIGN_UP",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
return Response(data, status=status.HTTP_200_OK)
|
return Response(data, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
@ -176,33 +239,92 @@ class SignInEndpoint(BaseAPIView):
|
|||||||
user.token_updated_at = timezone.now()
|
user.token_updated_at = timezone.now()
|
||||||
user.save()
|
user.save()
|
||||||
|
|
||||||
access_token, refresh_token = get_tokens_for_user(user)
|
# Check if user has any accepted invites for workspace and add them to workspace
|
||||||
# Send Analytics
|
workspace_member_invites = WorkspaceMemberInvite.objects.filter(
|
||||||
if settings.ANALYTICS_BASE_API:
|
email=user.email, accepted=True
|
||||||
_ = requests.post(
|
)
|
||||||
settings.ANALYTICS_BASE_API,
|
|
||||||
headers={
|
WorkspaceMember.objects.bulk_create(
|
||||||
"Content-Type": "application/json",
|
[
|
||||||
"X-Auth-Token": settings.ANALYTICS_SECRET_KEY,
|
WorkspaceMember(
|
||||||
},
|
workspace_id=workspace_member_invite.workspace_id,
|
||||||
json={
|
member=user,
|
||||||
"event_id": uuid.uuid4().hex,
|
role=workspace_member_invite.role,
|
||||||
"event_data": {
|
)
|
||||||
"medium": "email",
|
for workspace_member_invite in workspace_member_invites
|
||||||
|
],
|
||||||
|
ignore_conflicts=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check if user has any project invites
|
||||||
|
project_member_invites = ProjectMemberInvite.objects.filter(
|
||||||
|
email=user.email, accepted=True
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add user to workspace
|
||||||
|
WorkspaceMember.objects.bulk_create(
|
||||||
|
[
|
||||||
|
WorkspaceMember(
|
||||||
|
workspace_id=project_member_invite.workspace_id,
|
||||||
|
role=project_member_invite.role
|
||||||
|
if project_member_invite.role in [5, 10, 15]
|
||||||
|
else 15,
|
||||||
|
member=user,
|
||||||
|
created_by_id=project_member_invite.created_by_id,
|
||||||
|
)
|
||||||
|
for project_member_invite in project_member_invites
|
||||||
|
],
|
||||||
|
ignore_conflicts=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Now add the users to project
|
||||||
|
ProjectMember.objects.bulk_create(
|
||||||
|
[
|
||||||
|
ProjectMember(
|
||||||
|
workspace_id=project_member_invite.workspace_id,
|
||||||
|
role=project_member_invite.role
|
||||||
|
if project_member_invite.role in [5, 10, 15]
|
||||||
|
else 15,
|
||||||
|
member=user,
|
||||||
|
created_by_id=project_member_invite.created_by_id,
|
||||||
|
) for project_member_invite in project_member_invites
|
||||||
|
],
|
||||||
|
ignore_conflicts=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Delete all the invites
|
||||||
|
workspace_member_invites.delete()
|
||||||
|
project_member_invites.delete()
|
||||||
|
try:
|
||||||
|
# Send Analytics
|
||||||
|
if settings.ANALYTICS_BASE_API:
|
||||||
|
_ = requests.post(
|
||||||
|
settings.ANALYTICS_BASE_API,
|
||||||
|
headers={
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"X-Auth-Token": settings.ANALYTICS_SECRET_KEY,
|
||||||
},
|
},
|
||||||
"user": {"email": email, "id": str(user.id)},
|
json={
|
||||||
"device_ctx": {
|
"event_id": uuid.uuid4().hex,
|
||||||
"ip": request.META.get("REMOTE_ADDR"),
|
"event_data": {
|
||||||
"user_agent": request.META.get("HTTP_USER_AGENT"),
|
"medium": "email",
|
||||||
|
},
|
||||||
|
"user": {"email": email, "id": str(user.id)},
|
||||||
|
"device_ctx": {
|
||||||
|
"ip": request.META.get("REMOTE_ADDR"),
|
||||||
|
"user_agent": request.META.get("HTTP_USER_AGENT"),
|
||||||
|
},
|
||||||
|
"event_type": "SIGN_IN",
|
||||||
},
|
},
|
||||||
"event_type": "SIGN_IN",
|
)
|
||||||
},
|
except RequestException as e:
|
||||||
)
|
capture_exception(e)
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
"access_token": access_token,
|
"access_token": access_token,
|
||||||
"refresh_token": refresh_token,
|
"refresh_token": refresh_token,
|
||||||
}
|
}
|
||||||
|
access_token, refresh_token = get_tokens_for_user(user)
|
||||||
return Response(data, status=status.HTTP_200_OK)
|
return Response(data, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
@ -320,27 +442,37 @@ class MagicSignInEndpoint(BaseAPIView):
|
|||||||
if str(token) == str(user_token):
|
if str(token) == str(user_token):
|
||||||
if User.objects.filter(email=email).exists():
|
if User.objects.filter(email=email).exists():
|
||||||
user = User.objects.get(email=email)
|
user = User.objects.get(email=email)
|
||||||
# Send event to Jitsu for tracking
|
if not user.is_active:
|
||||||
if settings.ANALYTICS_BASE_API:
|
return Response(
|
||||||
_ = requests.post(
|
{
|
||||||
settings.ANALYTICS_BASE_API,
|
"error": "Your account has been deactivated. Please contact your site administrator."
|
||||||
headers={
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
"X-Auth-Token": settings.ANALYTICS_SECRET_KEY,
|
|
||||||
},
|
|
||||||
json={
|
|
||||||
"event_id": uuid.uuid4().hex,
|
|
||||||
"event_data": {
|
|
||||||
"medium": "code",
|
|
||||||
},
|
|
||||||
"user": {"email": email, "id": str(user.id)},
|
|
||||||
"device_ctx": {
|
|
||||||
"ip": request.META.get("REMOTE_ADDR"),
|
|
||||||
"user_agent": request.META.get("HTTP_USER_AGENT"),
|
|
||||||
},
|
|
||||||
"event_type": "SIGN_IN",
|
|
||||||
},
|
},
|
||||||
|
status=status.HTTP_403_FORBIDDEN,
|
||||||
)
|
)
|
||||||
|
try:
|
||||||
|
# Send event to Jitsu for tracking
|
||||||
|
if settings.ANALYTICS_BASE_API:
|
||||||
|
_ = requests.post(
|
||||||
|
settings.ANALYTICS_BASE_API,
|
||||||
|
headers={
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"X-Auth-Token": settings.ANALYTICS_SECRET_KEY,
|
||||||
|
},
|
||||||
|
json={
|
||||||
|
"event_id": uuid.uuid4().hex,
|
||||||
|
"event_data": {
|
||||||
|
"medium": "code",
|
||||||
|
},
|
||||||
|
"user": {"email": email, "id": str(user.id)},
|
||||||
|
"device_ctx": {
|
||||||
|
"ip": request.META.get("REMOTE_ADDR"),
|
||||||
|
"user_agent": request.META.get("HTTP_USER_AGENT"),
|
||||||
|
},
|
||||||
|
"event_type": "SIGN_IN",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
except RequestException as e:
|
||||||
|
capture_exception(e)
|
||||||
else:
|
else:
|
||||||
user = User.objects.create(
|
user = User.objects.create(
|
||||||
email=email,
|
email=email,
|
||||||
@ -348,27 +480,30 @@ class MagicSignInEndpoint(BaseAPIView):
|
|||||||
password=make_password(uuid.uuid4().hex),
|
password=make_password(uuid.uuid4().hex),
|
||||||
is_password_autoset=True,
|
is_password_autoset=True,
|
||||||
)
|
)
|
||||||
# Send event to Jitsu for tracking
|
try:
|
||||||
if settings.ANALYTICS_BASE_API:
|
# Send event to Jitsu for tracking
|
||||||
_ = requests.post(
|
if settings.ANALYTICS_BASE_API:
|
||||||
settings.ANALYTICS_BASE_API,
|
_ = requests.post(
|
||||||
headers={
|
settings.ANALYTICS_BASE_API,
|
||||||
"Content-Type": "application/json",
|
headers={
|
||||||
"X-Auth-Token": settings.ANALYTICS_SECRET_KEY,
|
"Content-Type": "application/json",
|
||||||
},
|
"X-Auth-Token": settings.ANALYTICS_SECRET_KEY,
|
||||||
json={
|
|
||||||
"event_id": uuid.uuid4().hex,
|
|
||||||
"event_data": {
|
|
||||||
"medium": "code",
|
|
||||||
},
|
},
|
||||||
"user": {"email": email, "id": str(user.id)},
|
json={
|
||||||
"device_ctx": {
|
"event_id": uuid.uuid4().hex,
|
||||||
"ip": request.META.get("REMOTE_ADDR"),
|
"event_data": {
|
||||||
"user_agent": request.META.get("HTTP_USER_AGENT"),
|
"medium": "code",
|
||||||
|
},
|
||||||
|
"user": {"email": email, "id": str(user.id)},
|
||||||
|
"device_ctx": {
|
||||||
|
"ip": request.META.get("REMOTE_ADDR"),
|
||||||
|
"user_agent": request.META.get("HTTP_USER_AGENT"),
|
||||||
|
},
|
||||||
|
"event_type": "SIGN_UP",
|
||||||
},
|
},
|
||||||
"event_type": "SIGN_UP",
|
)
|
||||||
},
|
except RequestException as e:
|
||||||
)
|
capture_exception(e)
|
||||||
|
|
||||||
user.last_active = timezone.now()
|
user.last_active = timezone.now()
|
||||||
user.last_login_time = timezone.now()
|
user.last_login_time = timezone.now()
|
||||||
@ -377,6 +512,63 @@ class MagicSignInEndpoint(BaseAPIView):
|
|||||||
user.token_updated_at = timezone.now()
|
user.token_updated_at = timezone.now()
|
||||||
user.save()
|
user.save()
|
||||||
|
|
||||||
|
# Check if user has any accepted invites for workspace and add them to workspace
|
||||||
|
workspace_member_invites = WorkspaceMemberInvite.objects.filter(
|
||||||
|
email=user.email, accepted=True
|
||||||
|
)
|
||||||
|
|
||||||
|
WorkspaceMember.objects.bulk_create(
|
||||||
|
[
|
||||||
|
WorkspaceMember(
|
||||||
|
workspace_id=workspace_member_invite.workspace_id,
|
||||||
|
member=user,
|
||||||
|
role=workspace_member_invite.role,
|
||||||
|
)
|
||||||
|
for workspace_member_invite in workspace_member_invites
|
||||||
|
],
|
||||||
|
ignore_conflicts=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check if user has any project invites
|
||||||
|
project_member_invites = ProjectMemberInvite.objects.filter(
|
||||||
|
email=user.email, accepted=True
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add user to workspace
|
||||||
|
WorkspaceMember.objects.bulk_create(
|
||||||
|
[
|
||||||
|
WorkspaceMember(
|
||||||
|
workspace_id=project_member_invite.workspace_id,
|
||||||
|
role=project_member_invite.role
|
||||||
|
if project_member_invite.role in [5, 10, 15]
|
||||||
|
else 15,
|
||||||
|
member=user,
|
||||||
|
created_by_id=project_member_invite.created_by_id,
|
||||||
|
)
|
||||||
|
for project_member_invite in project_member_invites
|
||||||
|
],
|
||||||
|
ignore_conflicts=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Now add the users to project
|
||||||
|
ProjectMember.objects.bulk_create(
|
||||||
|
[
|
||||||
|
ProjectMember(
|
||||||
|
workspace_id=project_member_invite.workspace_id,
|
||||||
|
role=project_member_invite.role
|
||||||
|
if project_member_invite.role in [5, 10, 15]
|
||||||
|
else 15,
|
||||||
|
member=user,
|
||||||
|
created_by_id=project_member_invite.created_by_id,
|
||||||
|
) for project_member_invite in project_member_invites
|
||||||
|
],
|
||||||
|
ignore_conflicts=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Delete all the invites
|
||||||
|
workspace_member_invites.delete()
|
||||||
|
project_member_invites.delete()
|
||||||
|
|
||||||
access_token, refresh_token = get_tokens_for_user(user)
|
access_token, refresh_token = get_tokens_for_user(user)
|
||||||
data = {
|
data = {
|
||||||
"access_token": access_token,
|
"access_token": access_token,
|
||||||
|
@ -64,9 +64,7 @@ class InboxViewSet(BaseViewSet):
|
|||||||
serializer.save(project_id=self.kwargs.get("project_id"))
|
serializer.save(project_id=self.kwargs.get("project_id"))
|
||||||
|
|
||||||
def destroy(self, request, slug, project_id, pk):
|
def destroy(self, request, slug, project_id, pk):
|
||||||
inbox = Inbox.objects.get(
|
inbox = Inbox.objects.get(workspace__slug=slug, project_id=project_id, pk=pk)
|
||||||
workspace__slug=slug, project_id=project_id, pk=pk
|
|
||||||
)
|
|
||||||
# Handle default inbox delete
|
# Handle default inbox delete
|
||||||
if inbox.is_default:
|
if inbox.is_default:
|
||||||
return Response(
|
return Response(
|
||||||
@ -128,9 +126,7 @@ class InboxIssueViewSet(BaseViewSet):
|
|||||||
.values("count")
|
.values("count")
|
||||||
)
|
)
|
||||||
.annotate(
|
.annotate(
|
||||||
attachment_count=IssueAttachment.objects.filter(
|
attachment_count=IssueAttachment.objects.filter(issue=OuterRef("id"))
|
||||||
issue=OuterRef("id")
|
|
||||||
)
|
|
||||||
.order_by()
|
.order_by()
|
||||||
.annotate(count=Func(F("id"), function="Count"))
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
.values("count")
|
.values("count")
|
||||||
@ -150,7 +146,6 @@ class InboxIssueViewSet(BaseViewSet):
|
|||||||
status=status.HTTP_200_OK,
|
status=status.HTTP_200_OK,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def create(self, request, slug, project_id, inbox_id):
|
def create(self, request, slug, project_id, inbox_id):
|
||||||
if not request.data.get("issue", {}).get("name", False):
|
if not request.data.get("issue", {}).get("name", False):
|
||||||
return Response(
|
return Response(
|
||||||
@ -198,7 +193,7 @@ class InboxIssueViewSet(BaseViewSet):
|
|||||||
issue_id=str(issue.id),
|
issue_id=str(issue.id),
|
||||||
project_id=str(project_id),
|
project_id=str(project_id),
|
||||||
current_instance=None,
|
current_instance=None,
|
||||||
epoch=int(timezone.now().timestamp())
|
epoch=int(timezone.now().timestamp()),
|
||||||
)
|
)
|
||||||
# create an inbox issue
|
# create an inbox issue
|
||||||
InboxIssue.objects.create(
|
InboxIssue.objects.create(
|
||||||
@ -216,10 +211,20 @@ class InboxIssueViewSet(BaseViewSet):
|
|||||||
pk=pk, workspace__slug=slug, project_id=project_id, inbox_id=inbox_id
|
pk=pk, workspace__slug=slug, project_id=project_id, inbox_id=inbox_id
|
||||||
)
|
)
|
||||||
# Get the project member
|
# Get the project member
|
||||||
project_member = ProjectMember.objects.get(workspace__slug=slug, project_id=project_id, member=request.user)
|
project_member = ProjectMember.objects.get(
|
||||||
|
workspace__slug=slug,
|
||||||
|
project_id=project_id,
|
||||||
|
member=request.user,
|
||||||
|
is_active=True,
|
||||||
|
)
|
||||||
# Only project members admins and created_by users can access this endpoint
|
# Only project members admins and created_by users can access this endpoint
|
||||||
if project_member.role <= 10 and str(inbox_issue.created_by_id) != str(request.user.id):
|
if project_member.role <= 10 and str(inbox_issue.created_by_id) != str(
|
||||||
return Response({"error": "You cannot edit inbox issues"}, status=status.HTTP_400_BAD_REQUEST)
|
request.user.id
|
||||||
|
):
|
||||||
|
return Response(
|
||||||
|
{"error": "You cannot edit inbox issues"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
# Get issue data
|
# Get issue data
|
||||||
issue_data = request.data.pop("issue", False)
|
issue_data = request.data.pop("issue", False)
|
||||||
@ -230,11 +235,13 @@ class InboxIssueViewSet(BaseViewSet):
|
|||||||
)
|
)
|
||||||
# Only allow guests and viewers to edit name and description
|
# Only allow guests and viewers to edit name and description
|
||||||
if project_member.role <= 10:
|
if project_member.role <= 10:
|
||||||
# viewers and guests since only viewers and guests
|
# viewers and guests since only viewers and guests
|
||||||
issue_data = {
|
issue_data = {
|
||||||
"name": issue_data.get("name", issue.name),
|
"name": issue_data.get("name", issue.name),
|
||||||
"description_html": issue_data.get("description_html", issue.description_html),
|
"description_html": issue_data.get(
|
||||||
"description": issue_data.get("description", issue.description)
|
"description_html", issue.description_html
|
||||||
|
),
|
||||||
|
"description": issue_data.get("description", issue.description),
|
||||||
}
|
}
|
||||||
|
|
||||||
issue_serializer = IssueCreateSerializer(
|
issue_serializer = IssueCreateSerializer(
|
||||||
@ -256,7 +263,7 @@ class InboxIssueViewSet(BaseViewSet):
|
|||||||
IssueSerializer(current_instance).data,
|
IssueSerializer(current_instance).data,
|
||||||
cls=DjangoJSONEncoder,
|
cls=DjangoJSONEncoder,
|
||||||
),
|
),
|
||||||
epoch=int(timezone.now().timestamp())
|
epoch=int(timezone.now().timestamp()),
|
||||||
)
|
)
|
||||||
issue_serializer.save()
|
issue_serializer.save()
|
||||||
else:
|
else:
|
||||||
@ -307,7 +314,9 @@ class InboxIssueViewSet(BaseViewSet):
|
|||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
else:
|
else:
|
||||||
return Response(InboxIssueSerializer(inbox_issue).data, status=status.HTTP_200_OK)
|
return Response(
|
||||||
|
InboxIssueSerializer(inbox_issue).data, status=status.HTTP_200_OK
|
||||||
|
)
|
||||||
|
|
||||||
def retrieve(self, request, slug, project_id, inbox_id, pk):
|
def retrieve(self, request, slug, project_id, inbox_id, pk):
|
||||||
inbox_issue = InboxIssue.objects.get(
|
inbox_issue = InboxIssue.objects.get(
|
||||||
@ -324,15 +333,27 @@ class InboxIssueViewSet(BaseViewSet):
|
|||||||
pk=pk, workspace__slug=slug, project_id=project_id, inbox_id=inbox_id
|
pk=pk, workspace__slug=slug, project_id=project_id, inbox_id=inbox_id
|
||||||
)
|
)
|
||||||
# Get the project member
|
# Get the project member
|
||||||
project_member = ProjectMember.objects.get(workspace__slug=slug, project_id=project_id, member=request.user)
|
project_member = ProjectMember.objects.get(
|
||||||
|
workspace__slug=slug,
|
||||||
|
project_id=project_id,
|
||||||
|
member=request.user,
|
||||||
|
is_active=True,
|
||||||
|
)
|
||||||
|
|
||||||
if project_member.role <= 10 and str(inbox_issue.created_by_id) != str(request.user.id):
|
if project_member.role <= 10 and str(inbox_issue.created_by_id) != str(
|
||||||
return Response({"error": "You cannot delete inbox issue"}, status=status.HTTP_400_BAD_REQUEST)
|
request.user.id
|
||||||
|
):
|
||||||
|
return Response(
|
||||||
|
{"error": "You cannot delete inbox issue"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
# Check the issue status
|
# Check the issue status
|
||||||
if inbox_issue.status in [-2, -1, 0, 2]:
|
if inbox_issue.status in [-2, -1, 0, 2]:
|
||||||
# Delete the issue also
|
# Delete the issue also
|
||||||
Issue.objects.filter(workspace__slug=slug, project_id=project_id, pk=inbox_issue.issue_id).delete()
|
Issue.objects.filter(
|
||||||
|
workspace__slug=slug, project_id=project_id, pk=inbox_issue.issue_id
|
||||||
|
).delete()
|
||||||
|
|
||||||
inbox_issue.delete()
|
inbox_issue.delete()
|
||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
@ -347,7 +368,10 @@ class InboxIssuePublicViewSet(BaseViewSet):
|
|||||||
]
|
]
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
project_deploy_board = ProjectDeployBoard.objects.get(workspace__slug=self.kwargs.get("slug"), project_id=self.kwargs.get("project_id"))
|
project_deploy_board = ProjectDeployBoard.objects.get(
|
||||||
|
workspace__slug=self.kwargs.get("slug"),
|
||||||
|
project_id=self.kwargs.get("project_id"),
|
||||||
|
)
|
||||||
if project_deploy_board is not None:
|
if project_deploy_board is not None:
|
||||||
return self.filter_queryset(
|
return self.filter_queryset(
|
||||||
super()
|
super()
|
||||||
@ -363,9 +387,14 @@ class InboxIssuePublicViewSet(BaseViewSet):
|
|||||||
return InboxIssue.objects.none()
|
return InboxIssue.objects.none()
|
||||||
|
|
||||||
def list(self, request, slug, project_id, inbox_id):
|
def list(self, request, slug, project_id, inbox_id):
|
||||||
project_deploy_board = ProjectDeployBoard.objects.get(workspace__slug=slug, project_id=project_id)
|
project_deploy_board = ProjectDeployBoard.objects.get(
|
||||||
|
workspace__slug=slug, project_id=project_id
|
||||||
|
)
|
||||||
if project_deploy_board.inbox is None:
|
if project_deploy_board.inbox is None:
|
||||||
return Response({"error": "Inbox is not enabled for this Project Board"}, status=status.HTTP_400_BAD_REQUEST)
|
return Response(
|
||||||
|
{"error": "Inbox is not enabled for this Project Board"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
filters = issue_filters(request.query_params, "GET")
|
filters = issue_filters(request.query_params, "GET")
|
||||||
issues = (
|
issues = (
|
||||||
@ -392,9 +421,7 @@ class InboxIssuePublicViewSet(BaseViewSet):
|
|||||||
.values("count")
|
.values("count")
|
||||||
)
|
)
|
||||||
.annotate(
|
.annotate(
|
||||||
attachment_count=IssueAttachment.objects.filter(
|
attachment_count=IssueAttachment.objects.filter(issue=OuterRef("id"))
|
||||||
issue=OuterRef("id")
|
|
||||||
)
|
|
||||||
.order_by()
|
.order_by()
|
||||||
.annotate(count=Func(F("id"), function="Count"))
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
.values("count")
|
.values("count")
|
||||||
@ -415,9 +442,14 @@ class InboxIssuePublicViewSet(BaseViewSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def create(self, request, slug, project_id, inbox_id):
|
def create(self, request, slug, project_id, inbox_id):
|
||||||
project_deploy_board = ProjectDeployBoard.objects.get(workspace__slug=slug, project_id=project_id)
|
project_deploy_board = ProjectDeployBoard.objects.get(
|
||||||
|
workspace__slug=slug, project_id=project_id
|
||||||
|
)
|
||||||
if project_deploy_board.inbox is None:
|
if project_deploy_board.inbox is None:
|
||||||
return Response({"error": "Inbox is not enabled for this Project Board"}, status=status.HTTP_400_BAD_REQUEST)
|
return Response(
|
||||||
|
{"error": "Inbox is not enabled for this Project Board"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
if not request.data.get("issue", {}).get("name", False):
|
if not request.data.get("issue", {}).get("name", False):
|
||||||
return Response(
|
return Response(
|
||||||
@ -465,7 +497,7 @@ class InboxIssuePublicViewSet(BaseViewSet):
|
|||||||
issue_id=str(issue.id),
|
issue_id=str(issue.id),
|
||||||
project_id=str(project_id),
|
project_id=str(project_id),
|
||||||
current_instance=None,
|
current_instance=None,
|
||||||
epoch=int(timezone.now().timestamp())
|
epoch=int(timezone.now().timestamp()),
|
||||||
)
|
)
|
||||||
# create an inbox issue
|
# create an inbox issue
|
||||||
InboxIssue.objects.create(
|
InboxIssue.objects.create(
|
||||||
@ -479,34 +511,41 @@ class InboxIssuePublicViewSet(BaseViewSet):
|
|||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
def partial_update(self, request, slug, project_id, inbox_id, pk):
|
def partial_update(self, request, slug, project_id, inbox_id, pk):
|
||||||
project_deploy_board = ProjectDeployBoard.objects.get(workspace__slug=slug, project_id=project_id)
|
project_deploy_board = ProjectDeployBoard.objects.get(
|
||||||
|
workspace__slug=slug, project_id=project_id
|
||||||
|
)
|
||||||
if project_deploy_board.inbox is None:
|
if project_deploy_board.inbox is None:
|
||||||
return Response({"error": "Inbox is not enabled for this Project Board"}, status=status.HTTP_400_BAD_REQUEST)
|
return Response(
|
||||||
|
{"error": "Inbox is not enabled for this Project Board"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
inbox_issue = InboxIssue.objects.get(
|
inbox_issue = InboxIssue.objects.get(
|
||||||
pk=pk, workspace__slug=slug, project_id=project_id, inbox_id=inbox_id
|
pk=pk, workspace__slug=slug, project_id=project_id, inbox_id=inbox_id
|
||||||
)
|
)
|
||||||
# Get the project member
|
# Get the project member
|
||||||
if str(inbox_issue.created_by_id) != str(request.user.id):
|
if str(inbox_issue.created_by_id) != str(request.user.id):
|
||||||
return Response({"error": "You cannot edit inbox issues"}, status=status.HTTP_400_BAD_REQUEST)
|
return Response(
|
||||||
|
{"error": "You cannot edit inbox issues"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
# Get issue data
|
# Get issue data
|
||||||
issue_data = request.data.pop("issue", False)
|
issue_data = request.data.pop("issue", False)
|
||||||
|
|
||||||
|
|
||||||
issue = Issue.objects.get(
|
issue = Issue.objects.get(
|
||||||
pk=inbox_issue.issue_id, workspace__slug=slug, project_id=project_id
|
pk=inbox_issue.issue_id, workspace__slug=slug, project_id=project_id
|
||||||
)
|
)
|
||||||
# viewers and guests since only viewers and guests
|
# viewers and guests since only viewers and guests
|
||||||
issue_data = {
|
issue_data = {
|
||||||
"name": issue_data.get("name", issue.name),
|
"name": issue_data.get("name", issue.name),
|
||||||
"description_html": issue_data.get("description_html", issue.description_html),
|
"description_html": issue_data.get(
|
||||||
"description": issue_data.get("description", issue.description)
|
"description_html", issue.description_html
|
||||||
|
),
|
||||||
|
"description": issue_data.get("description", issue.description),
|
||||||
}
|
}
|
||||||
|
|
||||||
issue_serializer = IssueCreateSerializer(
|
issue_serializer = IssueCreateSerializer(issue, data=issue_data, partial=True)
|
||||||
issue, data=issue_data, partial=True
|
|
||||||
)
|
|
||||||
|
|
||||||
if issue_serializer.is_valid():
|
if issue_serializer.is_valid():
|
||||||
current_instance = issue
|
current_instance = issue
|
||||||
@ -523,17 +562,22 @@ class InboxIssuePublicViewSet(BaseViewSet):
|
|||||||
IssueSerializer(current_instance).data,
|
IssueSerializer(current_instance).data,
|
||||||
cls=DjangoJSONEncoder,
|
cls=DjangoJSONEncoder,
|
||||||
),
|
),
|
||||||
epoch=int(timezone.now().timestamp())
|
epoch=int(timezone.now().timestamp()),
|
||||||
)
|
)
|
||||||
issue_serializer.save()
|
issue_serializer.save()
|
||||||
return Response(issue_serializer.data, status=status.HTTP_200_OK)
|
return Response(issue_serializer.data, status=status.HTTP_200_OK)
|
||||||
return Response(issue_serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
return Response(issue_serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
def retrieve(self, request, slug, project_id, inbox_id, pk):
|
def retrieve(self, request, slug, project_id, inbox_id, pk):
|
||||||
project_deploy_board = ProjectDeployBoard.objects.get(workspace__slug=slug, project_id=project_id)
|
project_deploy_board = ProjectDeployBoard.objects.get(
|
||||||
|
workspace__slug=slug, project_id=project_id
|
||||||
|
)
|
||||||
if project_deploy_board.inbox is None:
|
if project_deploy_board.inbox is None:
|
||||||
return Response({"error": "Inbox is not enabled for this Project Board"}, status=status.HTTP_400_BAD_REQUEST)
|
return Response(
|
||||||
|
{"error": "Inbox is not enabled for this Project Board"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
inbox_issue = InboxIssue.objects.get(
|
inbox_issue = InboxIssue.objects.get(
|
||||||
pk=pk, workspace__slug=slug, project_id=project_id, inbox_id=inbox_id
|
pk=pk, workspace__slug=slug, project_id=project_id, inbox_id=inbox_id
|
||||||
)
|
)
|
||||||
@ -544,16 +588,24 @@ class InboxIssuePublicViewSet(BaseViewSet):
|
|||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
def destroy(self, request, slug, project_id, inbox_id, pk):
|
def destroy(self, request, slug, project_id, inbox_id, pk):
|
||||||
project_deploy_board = ProjectDeployBoard.objects.get(workspace__slug=slug, project_id=project_id)
|
project_deploy_board = ProjectDeployBoard.objects.get(
|
||||||
|
workspace__slug=slug, project_id=project_id
|
||||||
|
)
|
||||||
if project_deploy_board.inbox is None:
|
if project_deploy_board.inbox is None:
|
||||||
return Response({"error": "Inbox is not enabled for this Project Board"}, status=status.HTTP_400_BAD_REQUEST)
|
return Response(
|
||||||
|
{"error": "Inbox is not enabled for this Project Board"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
inbox_issue = InboxIssue.objects.get(
|
inbox_issue = InboxIssue.objects.get(
|
||||||
pk=pk, workspace__slug=slug, project_id=project_id, inbox_id=inbox_id
|
pk=pk, workspace__slug=slug, project_id=project_id, inbox_id=inbox_id
|
||||||
)
|
)
|
||||||
|
|
||||||
if str(inbox_issue.created_by_id) != str(request.user.id):
|
if str(inbox_issue.created_by_id) != str(request.user.id):
|
||||||
return Response({"error": "You cannot delete inbox issue"}, status=status.HTTP_400_BAD_REQUEST)
|
return Response(
|
||||||
|
{"error": "You cannot delete inbox issue"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
inbox_issue.delete()
|
inbox_issue.delete()
|
||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
@ -623,6 +623,7 @@ class IssueCommentViewSet(BaseViewSet):
|
|||||||
workspace__slug=self.kwargs.get("slug"),
|
workspace__slug=self.kwargs.get("slug"),
|
||||||
project_id=self.kwargs.get("project_id"),
|
project_id=self.kwargs.get("project_id"),
|
||||||
member_id=self.request.user.id,
|
member_id=self.request.user.id,
|
||||||
|
is_active=True,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@ -1254,7 +1255,11 @@ class IssueSubscriberViewSet(BaseViewSet):
|
|||||||
|
|
||||||
def list(self, request, slug, project_id, issue_id):
|
def list(self, request, slug, project_id, issue_id):
|
||||||
members = (
|
members = (
|
||||||
ProjectMember.objects.filter(workspace__slug=slug, project_id=project_id)
|
ProjectMember.objects.filter(
|
||||||
|
workspace__slug=slug,
|
||||||
|
project_id=project_id,
|
||||||
|
is_active=True,
|
||||||
|
)
|
||||||
.annotate(
|
.annotate(
|
||||||
is_subscribed=Exists(
|
is_subscribed=Exists(
|
||||||
IssueSubscriber.objects.filter(
|
IssueSubscriber.objects.filter(
|
||||||
@ -1498,6 +1503,7 @@ class IssueCommentPublicViewSet(BaseViewSet):
|
|||||||
workspace__slug=self.kwargs.get("slug"),
|
workspace__slug=self.kwargs.get("slug"),
|
||||||
project_id=self.kwargs.get("project_id"),
|
project_id=self.kwargs.get("project_id"),
|
||||||
member_id=self.request.user.id,
|
member_id=self.request.user.id,
|
||||||
|
is_active=True,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@ -1538,6 +1544,7 @@ class IssueCommentPublicViewSet(BaseViewSet):
|
|||||||
if not ProjectMember.objects.filter(
|
if not ProjectMember.objects.filter(
|
||||||
project_id=project_id,
|
project_id=project_id,
|
||||||
member=request.user,
|
member=request.user,
|
||||||
|
is_active=True,
|
||||||
).exists():
|
).exists():
|
||||||
# Add the user for workspace tracking
|
# Add the user for workspace tracking
|
||||||
_ = ProjectPublicMember.objects.get_or_create(
|
_ = ProjectPublicMember.objects.get_or_create(
|
||||||
@ -1651,6 +1658,7 @@ class IssueReactionPublicViewSet(BaseViewSet):
|
|||||||
if not ProjectMember.objects.filter(
|
if not ProjectMember.objects.filter(
|
||||||
project_id=project_id,
|
project_id=project_id,
|
||||||
member=request.user,
|
member=request.user,
|
||||||
|
is_active=True,
|
||||||
).exists():
|
).exists():
|
||||||
# Add the user for workspace tracking
|
# Add the user for workspace tracking
|
||||||
_ = ProjectPublicMember.objects.get_or_create(
|
_ = ProjectPublicMember.objects.get_or_create(
|
||||||
@ -1744,7 +1752,9 @@ class CommentReactionPublicViewSet(BaseViewSet):
|
|||||||
project_id=project_id, comment_id=comment_id, actor=request.user
|
project_id=project_id, comment_id=comment_id, actor=request.user
|
||||||
)
|
)
|
||||||
if not ProjectMember.objects.filter(
|
if not ProjectMember.objects.filter(
|
||||||
project_id=project_id, member=request.user
|
project_id=project_id,
|
||||||
|
member=request.user,
|
||||||
|
is_active=True,
|
||||||
).exists():
|
).exists():
|
||||||
# Add the user for workspace tracking
|
# Add the user for workspace tracking
|
||||||
_ = ProjectPublicMember.objects.get_or_create(
|
_ = ProjectPublicMember.objects.get_or_create(
|
||||||
@ -1829,7 +1839,9 @@ class IssueVotePublicViewSet(BaseViewSet):
|
|||||||
)
|
)
|
||||||
# Add the user for workspace tracking
|
# Add the user for workspace tracking
|
||||||
if not ProjectMember.objects.filter(
|
if not ProjectMember.objects.filter(
|
||||||
project_id=project_id, member=request.user
|
project_id=project_id,
|
||||||
|
member=request.user,
|
||||||
|
is_active=True,
|
||||||
).exists():
|
).exists():
|
||||||
_ = ProjectPublicMember.objects.get_or_create(
|
_ = ProjectPublicMember.objects.get_or_create(
|
||||||
project_id=project_id,
|
project_id=project_id,
|
||||||
|
@ -85,7 +85,10 @@ class NotificationViewSet(BaseViewSet, BasePaginator):
|
|||||||
# Created issues
|
# Created issues
|
||||||
if type == "created":
|
if type == "created":
|
||||||
if WorkspaceMember.objects.filter(
|
if WorkspaceMember.objects.filter(
|
||||||
workspace__slug=slug, member=request.user, role__lt=15
|
workspace__slug=slug,
|
||||||
|
member=request.user,
|
||||||
|
role__lt=15,
|
||||||
|
is_active=True,
|
||||||
).exists():
|
).exists():
|
||||||
notifications = Notification.objects.none()
|
notifications = Notification.objects.none()
|
||||||
else:
|
else:
|
||||||
@ -255,7 +258,10 @@ class MarkAllReadNotificationViewSet(BaseViewSet):
|
|||||||
# Created issues
|
# Created issues
|
||||||
if type == "created":
|
if type == "created":
|
||||||
if WorkspaceMember.objects.filter(
|
if WorkspaceMember.objects.filter(
|
||||||
workspace__slug=slug, member=request.user, role__lt=15
|
workspace__slug=slug,
|
||||||
|
member=request.user,
|
||||||
|
role__lt=15,
|
||||||
|
is_active=True,
|
||||||
).exists():
|
).exists():
|
||||||
notifications = Notification.objects.none()
|
notifications = Notification.objects.none()
|
||||||
else:
|
else:
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
import uuid
|
import uuid
|
||||||
import requests
|
import requests
|
||||||
import os
|
import os
|
||||||
|
from requests.exceptions import RequestException
|
||||||
|
|
||||||
# Django imports
|
# Django imports
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
@ -20,7 +21,14 @@ from google.oauth2 import id_token
|
|||||||
from google.auth.transport import requests as google_auth_request
|
from google.auth.transport import requests as google_auth_request
|
||||||
|
|
||||||
# Module imports
|
# Module imports
|
||||||
from plane.db.models import SocialLoginConnection, User
|
from plane.db.models import (
|
||||||
|
SocialLoginConnection,
|
||||||
|
User,
|
||||||
|
WorkspaceMemberInvite,
|
||||||
|
WorkspaceMember,
|
||||||
|
ProjectMemberInvite,
|
||||||
|
ProjectMember,
|
||||||
|
)
|
||||||
from plane.api.serializers import UserSerializer
|
from plane.api.serializers import UserSerializer
|
||||||
from .base import BaseAPIView
|
from .base import BaseAPIView
|
||||||
|
|
||||||
@ -168,7 +176,6 @@ class OauthEndpoint(BaseAPIView):
|
|||||||
)
|
)
|
||||||
|
|
||||||
## Login Case
|
## Login Case
|
||||||
|
|
||||||
if not user.is_active:
|
if not user.is_active:
|
||||||
return Response(
|
return Response(
|
||||||
{
|
{
|
||||||
@ -185,12 +192,61 @@ class OauthEndpoint(BaseAPIView):
|
|||||||
user.is_email_verified = email_verified
|
user.is_email_verified = email_verified
|
||||||
user.save()
|
user.save()
|
||||||
|
|
||||||
access_token, refresh_token = get_tokens_for_user(user)
|
# Check if user has any accepted invites for workspace and add them to workspace
|
||||||
|
workspace_member_invites = WorkspaceMemberInvite.objects.filter(
|
||||||
|
email=user.email, accepted=True
|
||||||
|
)
|
||||||
|
|
||||||
data = {
|
WorkspaceMember.objects.bulk_create(
|
||||||
"access_token": access_token,
|
[
|
||||||
"refresh_token": refresh_token,
|
WorkspaceMember(
|
||||||
}
|
workspace_id=workspace_member_invite.workspace_id,
|
||||||
|
member=user,
|
||||||
|
role=workspace_member_invite.role,
|
||||||
|
)
|
||||||
|
for workspace_member_invite in workspace_member_invites
|
||||||
|
],
|
||||||
|
ignore_conflicts=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check if user has any project invites
|
||||||
|
project_member_invites = ProjectMemberInvite.objects.filter(
|
||||||
|
email=user.email, accepted=True
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add user to workspace
|
||||||
|
WorkspaceMember.objects.bulk_create(
|
||||||
|
[
|
||||||
|
WorkspaceMember(
|
||||||
|
workspace_id=project_member_invite.workspace_id,
|
||||||
|
role=project_member_invite.role
|
||||||
|
if project_member_invite.role in [5, 10, 15]
|
||||||
|
else 15,
|
||||||
|
member=user,
|
||||||
|
created_by_id=project_member_invite.created_by_id,
|
||||||
|
)
|
||||||
|
for project_member_invite in project_member_invites
|
||||||
|
],
|
||||||
|
ignore_conflicts=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Now add the users to project
|
||||||
|
ProjectMember.objects.bulk_create(
|
||||||
|
[
|
||||||
|
ProjectMember(
|
||||||
|
workspace_id=project_member_invite.workspace_id,
|
||||||
|
role=project_member_invite.role
|
||||||
|
if project_member_invite.role in [5, 10, 15]
|
||||||
|
else 15,
|
||||||
|
member=user,
|
||||||
|
created_by_id=project_member_invite.created_by_id,
|
||||||
|
) for project_member_invite in project_member_invites
|
||||||
|
],
|
||||||
|
ignore_conflicts=True,
|
||||||
|
)
|
||||||
|
# Delete all the invites
|
||||||
|
workspace_member_invites.delete()
|
||||||
|
project_member_invites.delete()
|
||||||
|
|
||||||
SocialLoginConnection.objects.update_or_create(
|
SocialLoginConnection.objects.update_or_create(
|
||||||
medium=medium,
|
medium=medium,
|
||||||
@ -201,26 +257,36 @@ class OauthEndpoint(BaseAPIView):
|
|||||||
"last_login_at": timezone.now(),
|
"last_login_at": timezone.now(),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
if settings.ANALYTICS_BASE_API:
|
try:
|
||||||
_ = requests.post(
|
if settings.ANALYTICS_BASE_API:
|
||||||
settings.ANALYTICS_BASE_API,
|
_ = requests.post(
|
||||||
headers={
|
settings.ANALYTICS_BASE_API,
|
||||||
"Content-Type": "application/json",
|
headers={
|
||||||
"X-Auth-Token": settings.ANALYTICS_SECRET_KEY,
|
"Content-Type": "application/json",
|
||||||
},
|
"X-Auth-Token": settings.ANALYTICS_SECRET_KEY,
|
||||||
json={
|
|
||||||
"event_id": uuid.uuid4().hex,
|
|
||||||
"event_data": {
|
|
||||||
"medium": f"oauth-{medium}",
|
|
||||||
},
|
},
|
||||||
"user": {"email": email, "id": str(user.id)},
|
json={
|
||||||
"device_ctx": {
|
"event_id": uuid.uuid4().hex,
|
||||||
"ip": request.META.get("REMOTE_ADDR"),
|
"event_data": {
|
||||||
"user_agent": request.META.get("HTTP_USER_AGENT"),
|
"medium": f"oauth-{medium}",
|
||||||
|
},
|
||||||
|
"user": {"email": email, "id": str(user.id)},
|
||||||
|
"device_ctx": {
|
||||||
|
"ip": request.META.get("REMOTE_ADDR"),
|
||||||
|
"user_agent": request.META.get("HTTP_USER_AGENT"),
|
||||||
|
},
|
||||||
|
"event_type": "SIGN_IN",
|
||||||
},
|
},
|
||||||
"event_type": "SIGN_IN",
|
)
|
||||||
},
|
except RequestException as e:
|
||||||
)
|
capture_exception(e)
|
||||||
|
|
||||||
|
access_token, refresh_token = get_tokens_for_user(user)
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"access_token": access_token,
|
||||||
|
"refresh_token": refresh_token,
|
||||||
|
}
|
||||||
return Response(data, status=status.HTTP_200_OK)
|
return Response(data, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
except User.DoesNotExist:
|
except User.DoesNotExist:
|
||||||
@ -260,31 +326,85 @@ class OauthEndpoint(BaseAPIView):
|
|||||||
user.token_updated_at = timezone.now()
|
user.token_updated_at = timezone.now()
|
||||||
user.save()
|
user.save()
|
||||||
|
|
||||||
access_token, refresh_token = get_tokens_for_user(user)
|
# Check if user has any accepted invites for workspace and add them to workspace
|
||||||
data = {
|
workspace_member_invites = WorkspaceMemberInvite.objects.filter(
|
||||||
"access_token": access_token,
|
email=user.email, accepted=True
|
||||||
"refresh_token": refresh_token,
|
)
|
||||||
}
|
|
||||||
if settings.ANALYTICS_BASE_API:
|
WorkspaceMember.objects.bulk_create(
|
||||||
_ = requests.post(
|
[
|
||||||
settings.ANALYTICS_BASE_API,
|
WorkspaceMember(
|
||||||
headers={
|
workspace_id=workspace_member_invite.workspace_id,
|
||||||
"Content-Type": "application/json",
|
member=user,
|
||||||
"X-Auth-Token": settings.ANALYTICS_SECRET_KEY,
|
role=workspace_member_invite.role,
|
||||||
},
|
)
|
||||||
json={
|
for workspace_member_invite in workspace_member_invites
|
||||||
"event_id": uuid.uuid4().hex,
|
],
|
||||||
"event_data": {
|
ignore_conflicts=True,
|
||||||
"medium": f"oauth-{medium}",
|
)
|
||||||
|
|
||||||
|
# Check if user has any project invites
|
||||||
|
project_member_invites = ProjectMemberInvite.objects.filter(
|
||||||
|
email=user.email, accepted=True
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add user to workspace
|
||||||
|
WorkspaceMember.objects.bulk_create(
|
||||||
|
[
|
||||||
|
WorkspaceMember(
|
||||||
|
workspace_id=project_member_invite.workspace_id,
|
||||||
|
role=project_member_invite.role
|
||||||
|
if project_member_invite.role in [5, 10, 15]
|
||||||
|
else 15,
|
||||||
|
member=user,
|
||||||
|
created_by_id=project_member_invite.created_by_id,
|
||||||
|
)
|
||||||
|
for project_member_invite in project_member_invites
|
||||||
|
],
|
||||||
|
ignore_conflicts=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Now add the users to project
|
||||||
|
ProjectMember.objects.bulk_create(
|
||||||
|
[
|
||||||
|
ProjectMember(
|
||||||
|
workspace_id=project_member_invite.workspace_id,
|
||||||
|
role=project_member_invite.role
|
||||||
|
if project_member_invite.role in [5, 10, 15]
|
||||||
|
else 15,
|
||||||
|
member=user,
|
||||||
|
created_by_id=project_member_invite.created_by_id,
|
||||||
|
) for project_member_invite in project_member_invites
|
||||||
|
],
|
||||||
|
ignore_conflicts=True,
|
||||||
|
)
|
||||||
|
# Delete all the invites
|
||||||
|
workspace_member_invites.delete()
|
||||||
|
project_member_invites.delete()
|
||||||
|
|
||||||
|
try:
|
||||||
|
if settings.ANALYTICS_BASE_API:
|
||||||
|
_ = requests.post(
|
||||||
|
settings.ANALYTICS_BASE_API,
|
||||||
|
headers={
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"X-Auth-Token": settings.ANALYTICS_SECRET_KEY,
|
||||||
},
|
},
|
||||||
"user": {"email": email, "id": str(user.id)},
|
json={
|
||||||
"device_ctx": {
|
"event_id": uuid.uuid4().hex,
|
||||||
"ip": request.META.get("REMOTE_ADDR"),
|
"event_data": {
|
||||||
"user_agent": request.META.get("HTTP_USER_AGENT"),
|
"medium": f"oauth-{medium}",
|
||||||
|
},
|
||||||
|
"user": {"email": email, "id": str(user.id)},
|
||||||
|
"device_ctx": {
|
||||||
|
"ip": request.META.get("REMOTE_ADDR"),
|
||||||
|
"user_agent": request.META.get("HTTP_USER_AGENT"),
|
||||||
|
},
|
||||||
|
"event_type": "SIGN_UP",
|
||||||
},
|
},
|
||||||
"event_type": "SIGN_UP",
|
)
|
||||||
},
|
except RequestException as e:
|
||||||
)
|
capture_exception(e)
|
||||||
|
|
||||||
SocialLoginConnection.objects.update_or_create(
|
SocialLoginConnection.objects.update_or_create(
|
||||||
medium=medium,
|
medium=medium,
|
||||||
@ -295,4 +415,10 @@ class OauthEndpoint(BaseAPIView):
|
|||||||
"last_login_at": timezone.now(),
|
"last_login_at": timezone.now(),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
access_token, refresh_token = get_tokens_for_user(user)
|
||||||
|
data = {
|
||||||
|
"access_token": access_token,
|
||||||
|
"refresh_token": refresh_token,
|
||||||
|
}
|
||||||
return Response(data, status=status.HTTP_201_CREATED)
|
return Response(data, status=status.HTTP_201_CREATED)
|
||||||
|
@ -17,13 +17,13 @@ from django.db.models import (
|
|||||||
)
|
)
|
||||||
from django.core.validators import validate_email
|
from django.core.validators import validate_email
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
# Third Party imports
|
# Third Party imports
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from rest_framework.permissions import AllowAny
|
from rest_framework.permissions import AllowAny
|
||||||
from sentry_sdk import capture_exception
|
|
||||||
|
|
||||||
# Module imports
|
# Module imports
|
||||||
from .base import BaseViewSet, BaseAPIView
|
from .base import BaseViewSet, BaseAPIView
|
||||||
@ -39,6 +39,7 @@ from plane.api.serializers import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
from plane.api.permissions import (
|
from plane.api.permissions import (
|
||||||
|
WorkspaceUserPermission,
|
||||||
ProjectBasePermission,
|
ProjectBasePermission,
|
||||||
ProjectEntityPermission,
|
ProjectEntityPermission,
|
||||||
ProjectMemberPermission,
|
ProjectMemberPermission,
|
||||||
@ -58,13 +59,6 @@ from plane.db.models import (
|
|||||||
ProjectIdentifier,
|
ProjectIdentifier,
|
||||||
Module,
|
Module,
|
||||||
Cycle,
|
Cycle,
|
||||||
CycleFavorite,
|
|
||||||
ModuleFavorite,
|
|
||||||
PageFavorite,
|
|
||||||
IssueViewFavorite,
|
|
||||||
Page,
|
|
||||||
IssueAssignee,
|
|
||||||
ModuleMember,
|
|
||||||
Inbox,
|
Inbox,
|
||||||
ProjectDeployBoard,
|
ProjectDeployBoard,
|
||||||
IssueProperty,
|
IssueProperty,
|
||||||
@ -110,12 +104,15 @@ class ProjectViewSet(BaseViewSet):
|
|||||||
member=self.request.user,
|
member=self.request.user,
|
||||||
project_id=OuterRef("pk"),
|
project_id=OuterRef("pk"),
|
||||||
workspace__slug=self.kwargs.get("slug"),
|
workspace__slug=self.kwargs.get("slug"),
|
||||||
|
is_active=True,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.annotate(
|
.annotate(
|
||||||
total_members=ProjectMember.objects.filter(
|
total_members=ProjectMember.objects.filter(
|
||||||
project_id=OuterRef("id"), member__is_bot=False
|
project_id=OuterRef("id"),
|
||||||
|
member__is_bot=False,
|
||||||
|
is_active=True,
|
||||||
)
|
)
|
||||||
.order_by()
|
.order_by()
|
||||||
.annotate(count=Func(F("id"), function="Count"))
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
@ -137,6 +134,7 @@ class ProjectViewSet(BaseViewSet):
|
|||||||
member_role=ProjectMember.objects.filter(
|
member_role=ProjectMember.objects.filter(
|
||||||
project_id=OuterRef("pk"),
|
project_id=OuterRef("pk"),
|
||||||
member_id=self.request.user.id,
|
member_id=self.request.user.id,
|
||||||
|
is_active=True,
|
||||||
).values("role")
|
).values("role")
|
||||||
)
|
)
|
||||||
.annotate(
|
.annotate(
|
||||||
@ -157,6 +155,7 @@ class ProjectViewSet(BaseViewSet):
|
|||||||
member=request.user,
|
member=request.user,
|
||||||
project_id=OuterRef("pk"),
|
project_id=OuterRef("pk"),
|
||||||
workspace__slug=self.kwargs.get("slug"),
|
workspace__slug=self.kwargs.get("slug"),
|
||||||
|
is_active=True,
|
||||||
).values("sort_order")
|
).values("sort_order")
|
||||||
projects = (
|
projects = (
|
||||||
self.get_queryset()
|
self.get_queryset()
|
||||||
@ -166,6 +165,7 @@ class ProjectViewSet(BaseViewSet):
|
|||||||
"project_projectmember",
|
"project_projectmember",
|
||||||
queryset=ProjectMember.objects.filter(
|
queryset=ProjectMember.objects.filter(
|
||||||
workspace__slug=slug,
|
workspace__slug=slug,
|
||||||
|
is_active=True,
|
||||||
).select_related("member"),
|
).select_related("member"),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@ -345,66 +345,104 @@ class ProjectViewSet(BaseViewSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class InviteProjectEndpoint(BaseAPIView):
|
class ProjectInvitationsViewset(BaseViewSet):
|
||||||
|
serializer_class = ProjectMemberInviteSerializer
|
||||||
|
model = ProjectMemberInvite
|
||||||
|
|
||||||
|
search_fields = []
|
||||||
|
|
||||||
permission_classes = [
|
permission_classes = [
|
||||||
ProjectBasePermission,
|
ProjectBasePermission,
|
||||||
]
|
]
|
||||||
|
|
||||||
def post(self, request, slug, project_id):
|
def get_queryset(self):
|
||||||
email = request.data.get("email", False)
|
return self.filter_queryset(
|
||||||
role = request.data.get("role", False)
|
super()
|
||||||
|
.get_queryset()
|
||||||
# Check if email is provided
|
.filter(workspace__slug=self.kwargs.get("slug"))
|
||||||
if not email:
|
.filter(project_id=self.kwargs.get("project_id"))
|
||||||
return Response(
|
.select_related("project")
|
||||||
{"error": "Email is required"}, status=status.HTTP_400_BAD_REQUEST
|
.select_related("workspace", "workspace__owner")
|
||||||
)
|
|
||||||
|
|
||||||
validate_email(email)
|
|
||||||
# Check if user is already a member of workspace
|
|
||||||
if ProjectMember.objects.filter(
|
|
||||||
project_id=project_id,
|
|
||||||
member__email=email,
|
|
||||||
member__is_bot=False,
|
|
||||||
).exists():
|
|
||||||
return Response(
|
|
||||||
{"error": "User is already member of workspace"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
|
|
||||||
user = User.objects.filter(email=email).first()
|
|
||||||
|
|
||||||
if user is None:
|
|
||||||
token = jwt.encode(
|
|
||||||
{"email": email, "timestamp": datetime.now().timestamp()},
|
|
||||||
settings.SECRET_KEY,
|
|
||||||
algorithm="HS256",
|
|
||||||
)
|
|
||||||
project_invitation_obj = ProjectMemberInvite.objects.create(
|
|
||||||
email=email.strip().lower(),
|
|
||||||
project_id=project_id,
|
|
||||||
token=token,
|
|
||||||
role=role,
|
|
||||||
)
|
|
||||||
domain = request.META.get('HTTP_ORIGIN')
|
|
||||||
project_invitation.delay(email, project_id, token, domain)
|
|
||||||
|
|
||||||
return Response(
|
|
||||||
{
|
|
||||||
"message": "Email sent successfully",
|
|
||||||
"id": project_invitation_obj.id,
|
|
||||||
},
|
|
||||||
status=status.HTTP_200_OK,
|
|
||||||
)
|
|
||||||
|
|
||||||
project_member = ProjectMember.objects.create(
|
|
||||||
member=user, project_id=project_id, role=role
|
|
||||||
)
|
)
|
||||||
|
|
||||||
_ = IssueProperty.objects.create(user=user, project_id=project_id)
|
def create(self, request, slug, project_id):
|
||||||
|
emails = request.data.get("emails", [])
|
||||||
|
|
||||||
|
# Check if email is provided
|
||||||
|
if not emails:
|
||||||
|
return Response(
|
||||||
|
{"error": "Emails are required"}, status=status.HTTP_400_BAD_REQUEST
|
||||||
|
)
|
||||||
|
|
||||||
|
requesting_user = ProjectMember.objects.get(
|
||||||
|
workspace__slug=slug, project_id=project_id, member_id=request.user.id
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check if any invited user has an higher role
|
||||||
|
if len(
|
||||||
|
[
|
||||||
|
email
|
||||||
|
for email in emails
|
||||||
|
if int(email.get("role", 10)) > requesting_user.role
|
||||||
|
]
|
||||||
|
):
|
||||||
|
return Response(
|
||||||
|
{"error": "You cannot invite a user with higher role"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
workspace = Workspace.objects.get(slug=slug)
|
||||||
|
|
||||||
|
project_invitations = []
|
||||||
|
for email in emails:
|
||||||
|
try:
|
||||||
|
validate_email(email.get("email"))
|
||||||
|
project_invitations.append(
|
||||||
|
ProjectMemberInvite(
|
||||||
|
email=email.get("email").strip().lower(),
|
||||||
|
project_id=project_id,
|
||||||
|
workspace_id=workspace.id,
|
||||||
|
token=jwt.encode(
|
||||||
|
{
|
||||||
|
"email": email,
|
||||||
|
"timestamp": datetime.now().timestamp(),
|
||||||
|
},
|
||||||
|
settings.SECRET_KEY,
|
||||||
|
algorithm="HS256",
|
||||||
|
),
|
||||||
|
role=email.get("role", 10),
|
||||||
|
created_by=request.user,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
except ValidationError:
|
||||||
|
return Response(
|
||||||
|
{
|
||||||
|
"error": f"Invalid email - {email} provided a valid email address is required to send the invite"
|
||||||
|
},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create workspace member invite
|
||||||
|
project_invitations = ProjectMemberInvite.objects.bulk_create(
|
||||||
|
project_invitations, batch_size=10, ignore_conflicts=True
|
||||||
|
)
|
||||||
|
current_site = f"{request.scheme}://{request.get_host()}",
|
||||||
|
|
||||||
|
# Send invitations
|
||||||
|
for invitation in project_invitations:
|
||||||
|
project_invitations.delay(
|
||||||
|
invitation.email,
|
||||||
|
project_id,
|
||||||
|
invitation.token,
|
||||||
|
current_site,
|
||||||
|
request.user.email,
|
||||||
|
)
|
||||||
|
|
||||||
return Response(
|
return Response(
|
||||||
ProjectMemberSerializer(project_member).data, status=status.HTTP_200_OK
|
{
|
||||||
|
"message": "Email sent successfully",
|
||||||
|
},
|
||||||
|
status=status.HTTP_200_OK,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -420,40 +458,134 @@ class UserProjectInvitationsViewset(BaseViewSet):
|
|||||||
.select_related("workspace", "workspace__owner", "project")
|
.select_related("workspace", "workspace__owner", "project")
|
||||||
)
|
)
|
||||||
|
|
||||||
def create(self, request):
|
def create(self, request, slug):
|
||||||
invitations = request.data.get("invitations")
|
project_ids = request.data.get("project_ids", [])
|
||||||
project_invitations = ProjectMemberInvite.objects.filter(
|
|
||||||
pk__in=invitations, accepted=True
|
# Get the workspace user role
|
||||||
|
workspace_member = WorkspaceMember.objects.get(
|
||||||
|
member=request.user,
|
||||||
|
workspace__slug=slug,
|
||||||
|
is_active=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
workspace_role = workspace_member.role
|
||||||
|
workspace = workspace_member.workspace
|
||||||
|
|
||||||
ProjectMember.objects.bulk_create(
|
ProjectMember.objects.bulk_create(
|
||||||
[
|
[
|
||||||
ProjectMember(
|
ProjectMember(
|
||||||
project=invitation.project,
|
project_id=project_id,
|
||||||
workspace=invitation.project.workspace,
|
|
||||||
member=request.user,
|
member=request.user,
|
||||||
role=invitation.role,
|
role=15 if workspace_role >= 15 else 10,
|
||||||
|
workspace=workspace,
|
||||||
created_by=request.user,
|
created_by=request.user,
|
||||||
)
|
)
|
||||||
for invitation in project_invitations
|
for project_id in project_ids
|
||||||
]
|
],
|
||||||
|
ignore_conflicts=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
IssueProperty.objects.bulk_create(
|
IssueProperty.objects.bulk_create(
|
||||||
[
|
[
|
||||||
ProjectMember(
|
IssueProperty(
|
||||||
project=invitation.project,
|
project_id=project_id,
|
||||||
workspace=invitation.project.workspace,
|
|
||||||
user=request.user,
|
user=request.user,
|
||||||
|
workspace=workspace,
|
||||||
created_by=request.user,
|
created_by=request.user,
|
||||||
)
|
)
|
||||||
for invitation in project_invitations
|
for project_id in project_ids
|
||||||
]
|
],
|
||||||
|
ignore_conflicts=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Delete joined project invites
|
return Response(
|
||||||
project_invitations.delete()
|
{"message": "Projects joined successfully"},
|
||||||
|
status=status.HTTP_201_CREATED,
|
||||||
|
)
|
||||||
|
|
||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
|
||||||
|
class ProjectJoinEndpoint(BaseAPIView):
|
||||||
|
permission_classes = [
|
||||||
|
AllowAny,
|
||||||
|
]
|
||||||
|
|
||||||
|
def post(self, request, slug, project_id, pk):
|
||||||
|
project_invite = ProjectMemberInvite.objects.get(
|
||||||
|
pk=pk,
|
||||||
|
project_id=project_id,
|
||||||
|
workspace__slug=slug,
|
||||||
|
)
|
||||||
|
|
||||||
|
email = request.data.get("email", "")
|
||||||
|
|
||||||
|
if email == "" or project_invite.email != email:
|
||||||
|
return Response(
|
||||||
|
{"error": "You do not have permission to join the project"},
|
||||||
|
status=status.HTTP_403_FORBIDDEN,
|
||||||
|
)
|
||||||
|
|
||||||
|
if project_invite.responded_at is None:
|
||||||
|
project_invite.accepted = request.data.get("accepted", False)
|
||||||
|
project_invite.responded_at = timezone.now()
|
||||||
|
project_invite.save()
|
||||||
|
|
||||||
|
if project_invite.accepted:
|
||||||
|
# Check if the user account exists
|
||||||
|
user = User.objects.filter(email=email).first()
|
||||||
|
|
||||||
|
# Check if user is a part of workspace
|
||||||
|
workspace_member = WorkspaceMember.objects.filter(
|
||||||
|
workspace__slug=slug, member=user
|
||||||
|
).first()
|
||||||
|
# Add him to workspace
|
||||||
|
if workspace_member is None:
|
||||||
|
_ = WorkspaceMember.objects.create(
|
||||||
|
workspace_id=project_invite.workspace_id,
|
||||||
|
member=user,
|
||||||
|
role=15 if project_invite.role >= 15 else project_invite.role,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# Else make him active
|
||||||
|
workspace_member.is_active = True
|
||||||
|
workspace_member.save()
|
||||||
|
|
||||||
|
# Check if the user was already a member of project then activate the user
|
||||||
|
project_member = ProjectMember.objects.filter(
|
||||||
|
workspace_id=project_invite.workspace_id, member=user
|
||||||
|
).first()
|
||||||
|
if project_member is None:
|
||||||
|
# Create a Project Member
|
||||||
|
_ = ProjectMember.objects.create(
|
||||||
|
workspace_id=project_invite.workspace_id,
|
||||||
|
member=user,
|
||||||
|
role=project_invite.role,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
project_member.is_active = True
|
||||||
|
project_member.role = project_member.role
|
||||||
|
project_member.save()
|
||||||
|
|
||||||
|
return Response(
|
||||||
|
{"message": "Project Invitation Accepted"},
|
||||||
|
status=status.HTTP_200_OK,
|
||||||
|
)
|
||||||
|
|
||||||
|
return Response(
|
||||||
|
{"message": "Project Invitation was not accepted"},
|
||||||
|
status=status.HTTP_200_OK,
|
||||||
|
)
|
||||||
|
|
||||||
|
return Response(
|
||||||
|
{"error": "You have already responded to the invitation request"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
def get(self, request, slug, project_id, pk):
|
||||||
|
project_invitation = ProjectMemberInvite.objects.get(
|
||||||
|
workspace__slug=slug, project_id=project_id, pk=pk
|
||||||
|
)
|
||||||
|
serializer = ProjectMemberInviteSerializer(project_invitation)
|
||||||
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
class ProjectMemberViewSet(BaseViewSet):
|
class ProjectMemberViewSet(BaseViewSet):
|
||||||
@ -475,6 +607,7 @@ class ProjectMemberViewSet(BaseViewSet):
|
|||||||
.filter(workspace__slug=self.kwargs.get("slug"))
|
.filter(workspace__slug=self.kwargs.get("slug"))
|
||||||
.filter(project_id=self.kwargs.get("project_id"))
|
.filter(project_id=self.kwargs.get("project_id"))
|
||||||
.filter(member__is_bot=False)
|
.filter(member__is_bot=False)
|
||||||
|
.filter()
|
||||||
.select_related("project")
|
.select_related("project")
|
||||||
.select_related("member")
|
.select_related("member")
|
||||||
.select_related("workspace", "workspace__owner")
|
.select_related("workspace", "workspace__owner")
|
||||||
@ -542,13 +675,17 @@ class ProjectMemberViewSet(BaseViewSet):
|
|||||||
|
|
||||||
def list(self, request, slug, project_id):
|
def list(self, request, slug, project_id):
|
||||||
project_member = ProjectMember.objects.get(
|
project_member = ProjectMember.objects.get(
|
||||||
member=request.user, workspace__slug=slug, project_id=project_id
|
member=request.user,
|
||||||
|
workspace__slug=slug,
|
||||||
|
project_id=project_id,
|
||||||
|
is_active=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
project_members = ProjectMember.objects.filter(
|
project_members = ProjectMember.objects.filter(
|
||||||
project_id=project_id,
|
project_id=project_id,
|
||||||
workspace__slug=slug,
|
workspace__slug=slug,
|
||||||
member__is_bot=False,
|
member__is_bot=False,
|
||||||
|
is_active=True,
|
||||||
).select_related("project", "member", "workspace")
|
).select_related("project", "member", "workspace")
|
||||||
|
|
||||||
if project_member.role > 10:
|
if project_member.role > 10:
|
||||||
@ -559,7 +696,10 @@ class ProjectMemberViewSet(BaseViewSet):
|
|||||||
|
|
||||||
def partial_update(self, request, slug, project_id, pk):
|
def partial_update(self, request, slug, project_id, pk):
|
||||||
project_member = ProjectMember.objects.get(
|
project_member = ProjectMember.objects.get(
|
||||||
pk=pk, workspace__slug=slug, project_id=project_id
|
pk=pk,
|
||||||
|
workspace__slug=slug,
|
||||||
|
project_id=project_id,
|
||||||
|
is_active=True,
|
||||||
)
|
)
|
||||||
if request.user.id == project_member.member_id:
|
if request.user.id == project_member.member_id:
|
||||||
return Response(
|
return Response(
|
||||||
@ -568,7 +708,10 @@ class ProjectMemberViewSet(BaseViewSet):
|
|||||||
)
|
)
|
||||||
# Check while updating user roles
|
# Check while updating user roles
|
||||||
requested_project_member = ProjectMember.objects.get(
|
requested_project_member = ProjectMember.objects.get(
|
||||||
project_id=project_id, workspace__slug=slug, member=request.user
|
project_id=project_id,
|
||||||
|
workspace__slug=slug,
|
||||||
|
member=request.user,
|
||||||
|
is_active=True,
|
||||||
)
|
)
|
||||||
if (
|
if (
|
||||||
"role" in request.data
|
"role" in request.data
|
||||||
@ -591,54 +734,66 @@ class ProjectMemberViewSet(BaseViewSet):
|
|||||||
|
|
||||||
def destroy(self, request, slug, project_id, pk):
|
def destroy(self, request, slug, project_id, pk):
|
||||||
project_member = ProjectMember.objects.get(
|
project_member = ProjectMember.objects.get(
|
||||||
workspace__slug=slug, project_id=project_id, pk=pk
|
workspace__slug=slug,
|
||||||
|
project_id=project_id,
|
||||||
|
pk=pk,
|
||||||
|
member__is_bot=False,
|
||||||
|
is_active=True,
|
||||||
)
|
)
|
||||||
# check requesting user role
|
# check requesting user role
|
||||||
requesting_project_member = ProjectMember.objects.get(
|
requesting_project_member = ProjectMember.objects.get(
|
||||||
workspace__slug=slug, member=request.user, project_id=project_id
|
workspace__slug=slug,
|
||||||
|
member=request.user,
|
||||||
|
project_id=project_id,
|
||||||
|
is_active=True,
|
||||||
)
|
)
|
||||||
|
# User cannot remove himself
|
||||||
|
if str(project_member.id) == str(requesting_project_member.id):
|
||||||
|
return Response(
|
||||||
|
{
|
||||||
|
"error": "You cannot remove yourself from the workspace. Please use leave workspace"
|
||||||
|
},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
# User cannot deactivate higher role
|
||||||
if requesting_project_member.role < project_member.role:
|
if requesting_project_member.role < project_member.role:
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "You cannot remove a user having role higher than yourself"},
|
{"error": "You cannot remove a user having role higher than you"},
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Remove all favorites
|
project_member.is_active = False
|
||||||
ProjectFavorite.objects.filter(
|
project_member.save()
|
||||||
workspace__slug=slug, project_id=project_id, user=project_member.member
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
).delete()
|
|
||||||
CycleFavorite.objects.filter(
|
|
||||||
workspace__slug=slug, project_id=project_id, user=project_member.member
|
|
||||||
).delete()
|
|
||||||
ModuleFavorite.objects.filter(
|
|
||||||
workspace__slug=slug, project_id=project_id, user=project_member.member
|
|
||||||
).delete()
|
|
||||||
PageFavorite.objects.filter(
|
|
||||||
workspace__slug=slug, project_id=project_id, user=project_member.member
|
|
||||||
).delete()
|
|
||||||
IssueViewFavorite.objects.filter(
|
|
||||||
workspace__slug=slug, project_id=project_id, user=project_member.member
|
|
||||||
).delete()
|
|
||||||
# Also remove issue from issue assigned
|
|
||||||
IssueAssignee.objects.filter(
|
|
||||||
workspace__slug=slug,
|
|
||||||
project_id=project_id,
|
|
||||||
assignee=project_member.member,
|
|
||||||
).delete()
|
|
||||||
|
|
||||||
# Remove if module member
|
def leave(self, request, slug, project_id):
|
||||||
ModuleMember.objects.filter(
|
project_member = ProjectMember.objects.get(
|
||||||
workspace__slug=slug,
|
workspace__slug=slug,
|
||||||
project_id=project_id,
|
project_id=project_id,
|
||||||
member=project_member.member,
|
member=request.user,
|
||||||
).delete()
|
is_active=True,
|
||||||
# Delete owned Pages
|
)
|
||||||
Page.objects.filter(
|
|
||||||
workspace__slug=slug,
|
# Check if the leaving user is the only admin of the project
|
||||||
project_id=project_id,
|
if (
|
||||||
owned_by=project_member.member,
|
project_member.role == 20
|
||||||
).delete()
|
and not ProjectMember.objects.filter(
|
||||||
project_member.delete()
|
workspace__slug=slug,
|
||||||
|
project_id=project_id,
|
||||||
|
role=20,
|
||||||
|
is_active=True,
|
||||||
|
).count()
|
||||||
|
> 1
|
||||||
|
):
|
||||||
|
return Response(
|
||||||
|
{
|
||||||
|
"error": "You cannot leave the project as your the only admin of the project you will have to either delete the project or create an another admin",
|
||||||
|
},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
# Deactivate the user
|
||||||
|
project_member.is_active = False
|
||||||
|
project_member.save()
|
||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
|
||||||
|
|
||||||
@ -691,46 +846,6 @@ class AddTeamToProjectEndpoint(BaseAPIView):
|
|||||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||||
|
|
||||||
|
|
||||||
class ProjectMemberInvitationsViewset(BaseViewSet):
|
|
||||||
serializer_class = ProjectMemberInviteSerializer
|
|
||||||
model = ProjectMemberInvite
|
|
||||||
|
|
||||||
search_fields = []
|
|
||||||
|
|
||||||
permission_classes = [
|
|
||||||
ProjectBasePermission,
|
|
||||||
]
|
|
||||||
|
|
||||||
def get_queryset(self):
|
|
||||||
return self.filter_queryset(
|
|
||||||
super()
|
|
||||||
.get_queryset()
|
|
||||||
.filter(workspace__slug=self.kwargs.get("slug"))
|
|
||||||
.filter(project_id=self.kwargs.get("project_id"))
|
|
||||||
.select_related("project")
|
|
||||||
.select_related("workspace", "workspace__owner")
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class ProjectMemberInviteDetailViewSet(BaseViewSet):
|
|
||||||
serializer_class = ProjectMemberInviteSerializer
|
|
||||||
model = ProjectMemberInvite
|
|
||||||
|
|
||||||
search_fields = []
|
|
||||||
|
|
||||||
permission_classes = [
|
|
||||||
ProjectBasePermission,
|
|
||||||
]
|
|
||||||
|
|
||||||
def get_queryset(self):
|
|
||||||
return self.filter_queryset(
|
|
||||||
super()
|
|
||||||
.get_queryset()
|
|
||||||
.select_related("project")
|
|
||||||
.select_related("workspace", "workspace__owner")
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class ProjectIdentifierEndpoint(BaseAPIView):
|
class ProjectIdentifierEndpoint(BaseAPIView):
|
||||||
permission_classes = [
|
permission_classes = [
|
||||||
ProjectBasePermission,
|
ProjectBasePermission,
|
||||||
@ -774,59 +889,14 @@ class ProjectIdentifierEndpoint(BaseAPIView):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class ProjectJoinEndpoint(BaseAPIView):
|
|
||||||
def post(self, request, slug):
|
|
||||||
project_ids = request.data.get("project_ids", [])
|
|
||||||
|
|
||||||
# Get the workspace user role
|
|
||||||
workspace_member = WorkspaceMember.objects.get(
|
|
||||||
member=request.user, workspace__slug=slug
|
|
||||||
)
|
|
||||||
|
|
||||||
workspace_role = workspace_member.role
|
|
||||||
workspace = workspace_member.workspace
|
|
||||||
|
|
||||||
ProjectMember.objects.bulk_create(
|
|
||||||
[
|
|
||||||
ProjectMember(
|
|
||||||
project_id=project_id,
|
|
||||||
member=request.user,
|
|
||||||
role=20
|
|
||||||
if workspace_role >= 15
|
|
||||||
else (15 if workspace_role == 10 else workspace_role),
|
|
||||||
workspace=workspace,
|
|
||||||
created_by=request.user,
|
|
||||||
)
|
|
||||||
for project_id in project_ids
|
|
||||||
],
|
|
||||||
ignore_conflicts=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
IssueProperty.objects.bulk_create(
|
|
||||||
[
|
|
||||||
IssueProperty(
|
|
||||||
project_id=project_id,
|
|
||||||
user=request.user,
|
|
||||||
workspace=workspace,
|
|
||||||
created_by=request.user,
|
|
||||||
)
|
|
||||||
for project_id in project_ids
|
|
||||||
],
|
|
||||||
ignore_conflicts=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
return Response(
|
|
||||||
{"message": "Projects joined successfully"},
|
|
||||||
status=status.HTTP_201_CREATED,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class ProjectUserViewsEndpoint(BaseAPIView):
|
class ProjectUserViewsEndpoint(BaseAPIView):
|
||||||
def post(self, request, slug, project_id):
|
def post(self, request, slug, project_id):
|
||||||
project = Project.objects.get(pk=project_id, workspace__slug=slug)
|
project = Project.objects.get(pk=project_id, workspace__slug=slug)
|
||||||
|
|
||||||
project_member = ProjectMember.objects.filter(
|
project_member = ProjectMember.objects.filter(
|
||||||
member=request.user, project=project
|
member=request.user,
|
||||||
|
project=project,
|
||||||
|
is_active=True,
|
||||||
).first()
|
).first()
|
||||||
|
|
||||||
if project_member is None:
|
if project_member is None:
|
||||||
@ -850,7 +920,10 @@ class ProjectUserViewsEndpoint(BaseAPIView):
|
|||||||
class ProjectMemberUserEndpoint(BaseAPIView):
|
class ProjectMemberUserEndpoint(BaseAPIView):
|
||||||
def get(self, request, slug, project_id):
|
def get(self, request, slug, project_id):
|
||||||
project_member = ProjectMember.objects.get(
|
project_member = ProjectMember.objects.get(
|
||||||
project_id=project_id, workspace__slug=slug, member=request.user
|
project_id=project_id,
|
||||||
|
workspace__slug=slug,
|
||||||
|
member=request.user,
|
||||||
|
is_active=True,
|
||||||
)
|
)
|
||||||
serializer = ProjectMemberSerializer(project_member)
|
serializer = ProjectMemberSerializer(project_member)
|
||||||
|
|
||||||
@ -983,39 +1056,6 @@ class WorkspaceProjectDeployBoardEndpoint(BaseAPIView):
|
|||||||
return Response(projects, status=status.HTTP_200_OK)
|
return Response(projects, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
class LeaveProjectEndpoint(BaseAPIView):
|
|
||||||
permission_classes = [
|
|
||||||
ProjectLitePermission,
|
|
||||||
]
|
|
||||||
|
|
||||||
def delete(self, request, slug, project_id):
|
|
||||||
project_member = ProjectMember.objects.get(
|
|
||||||
workspace__slug=slug,
|
|
||||||
member=request.user,
|
|
||||||
project_id=project_id,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Only Admin case
|
|
||||||
if (
|
|
||||||
project_member.role == 20
|
|
||||||
and ProjectMember.objects.filter(
|
|
||||||
workspace__slug=slug,
|
|
||||||
role=20,
|
|
||||||
project_id=project_id,
|
|
||||||
).count()
|
|
||||||
== 1
|
|
||||||
):
|
|
||||||
return Response(
|
|
||||||
{
|
|
||||||
"error": "You cannot leave the project since you are the only admin of the project you should delete the project"
|
|
||||||
},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
# Delete the member from workspace
|
|
||||||
project_member.delete()
|
|
||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
|
||||||
|
|
||||||
|
|
||||||
class ProjectPublicCoverImagesEndpoint(BaseAPIView):
|
class ProjectPublicCoverImagesEndpoint(BaseAPIView):
|
||||||
permission_classes = [
|
permission_classes = [
|
||||||
AllowAny,
|
AllowAny,
|
||||||
|
@ -13,13 +13,7 @@ from plane.api.serializers import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
from plane.api.views.base import BaseViewSet, BaseAPIView
|
from plane.api.views.base import BaseViewSet, BaseAPIView
|
||||||
from plane.db.models import (
|
from plane.db.models import User, IssueActivity, WorkspaceMember
|
||||||
User,
|
|
||||||
Workspace,
|
|
||||||
WorkspaceMemberInvite,
|
|
||||||
Issue,
|
|
||||||
IssueActivity,
|
|
||||||
)
|
|
||||||
from plane.utils.paginator import BasePaginator
|
from plane.utils.paginator import BasePaginator
|
||||||
|
|
||||||
|
|
||||||
@ -41,10 +35,28 @@ class UserEndpoint(BaseViewSet):
|
|||||||
serialized_data = UserMeSettingsSerializer(request.user).data
|
serialized_data = UserMeSettingsSerializer(request.user).data
|
||||||
return Response(serialized_data, status=status.HTTP_200_OK)
|
return Response(serialized_data, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
def deactivate(self, request):
|
||||||
|
# Check all workspace user is active
|
||||||
|
user = self.get_object()
|
||||||
|
if WorkspaceMember.objects.filter(
|
||||||
|
member=request.user, is_active=True
|
||||||
|
).exists():
|
||||||
|
return Response(
|
||||||
|
{
|
||||||
|
"error": "User cannot deactivate account as user is active in some workspaces"
|
||||||
|
},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Deactivate the user
|
||||||
|
user.is_active = False
|
||||||
|
user.save()
|
||||||
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
|
||||||
|
|
||||||
class UpdateUserOnBoardedEndpoint(BaseAPIView):
|
class UpdateUserOnBoardedEndpoint(BaseAPIView):
|
||||||
def patch(self, request):
|
def patch(self, request):
|
||||||
user = User.objects.get(pk=request.user.id)
|
user = User.objects.get(pk=request.user.id, is_active=True)
|
||||||
user.is_onboarded = request.data.get("is_onboarded", False)
|
user.is_onboarded = request.data.get("is_onboarded", False)
|
||||||
user.save()
|
user.save()
|
||||||
return Response({"message": "Updated successfully"}, status=status.HTTP_200_OK)
|
return Response({"message": "Updated successfully"}, status=status.HTTP_200_OK)
|
||||||
@ -52,7 +64,7 @@ class UpdateUserOnBoardedEndpoint(BaseAPIView):
|
|||||||
|
|
||||||
class UpdateUserTourCompletedEndpoint(BaseAPIView):
|
class UpdateUserTourCompletedEndpoint(BaseAPIView):
|
||||||
def patch(self, request):
|
def patch(self, request):
|
||||||
user = User.objects.get(pk=request.user.id)
|
user = User.objects.get(pk=request.user.id, is_active=True)
|
||||||
user.is_tour_completed = request.data.get("is_tour_completed", False)
|
user.is_tour_completed = request.data.get("is_tour_completed", False)
|
||||||
user.save()
|
user.save()
|
||||||
return Response({"message": "Updated successfully"}, status=status.HTTP_200_OK)
|
return Response({"message": "Updated successfully"}, status=status.HTTP_200_OK)
|
||||||
|
@ -2,7 +2,6 @@
|
|||||||
import jwt
|
import jwt
|
||||||
from datetime import date, datetime
|
from datetime import date, datetime
|
||||||
from dateutil.relativedelta import relativedelta
|
from dateutil.relativedelta import relativedelta
|
||||||
from uuid import uuid4
|
|
||||||
|
|
||||||
# Django imports
|
# Django imports
|
||||||
from django.db import IntegrityError
|
from django.db import IntegrityError
|
||||||
@ -26,13 +25,11 @@ from django.db.models import (
|
|||||||
)
|
)
|
||||||
from django.db.models.functions import ExtractWeek, Cast, ExtractDay
|
from django.db.models.functions import ExtractWeek, Cast, ExtractDay
|
||||||
from django.db.models.fields import DateField
|
from django.db.models.fields import DateField
|
||||||
from django.contrib.auth.hashers import make_password
|
|
||||||
|
|
||||||
# Third party modules
|
# Third party modules
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.permissions import AllowAny
|
from rest_framework.permissions import AllowAny, IsAuthenticated
|
||||||
from sentry_sdk import capture_exception
|
|
||||||
|
|
||||||
# Module imports
|
# Module imports
|
||||||
from plane.api.serializers import (
|
from plane.api.serializers import (
|
||||||
@ -59,14 +56,6 @@ from plane.db.models import (
|
|||||||
IssueActivity,
|
IssueActivity,
|
||||||
Issue,
|
Issue,
|
||||||
WorkspaceTheme,
|
WorkspaceTheme,
|
||||||
IssueAssignee,
|
|
||||||
ProjectFavorite,
|
|
||||||
CycleFavorite,
|
|
||||||
ModuleMember,
|
|
||||||
ModuleFavorite,
|
|
||||||
PageFavorite,
|
|
||||||
Page,
|
|
||||||
IssueViewFavorite,
|
|
||||||
IssueLink,
|
IssueLink,
|
||||||
IssueAttachment,
|
IssueAttachment,
|
||||||
IssueSubscriber,
|
IssueSubscriber,
|
||||||
@ -106,7 +95,9 @@ class WorkSpaceViewSet(BaseViewSet):
|
|||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
member_count = (
|
member_count = (
|
||||||
WorkspaceMember.objects.filter(
|
WorkspaceMember.objects.filter(
|
||||||
workspace=OuterRef("id"), member__is_bot=False
|
workspace=OuterRef("id"),
|
||||||
|
member__is_bot=False,
|
||||||
|
is_active=True,
|
||||||
)
|
)
|
||||||
.order_by()
|
.order_by()
|
||||||
.annotate(count=Func(F("id"), function="Count"))
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
@ -181,7 +172,9 @@ class UserWorkSpacesEndpoint(BaseAPIView):
|
|||||||
def get(self, request):
|
def get(self, request):
|
||||||
member_count = (
|
member_count = (
|
||||||
WorkspaceMember.objects.filter(
|
WorkspaceMember.objects.filter(
|
||||||
workspace=OuterRef("id"), member__is_bot=False
|
workspace=OuterRef("id"),
|
||||||
|
member__is_bot=False,
|
||||||
|
is_active=True,
|
||||||
)
|
)
|
||||||
.order_by()
|
.order_by()
|
||||||
.annotate(count=Func(F("id"), function="Count"))
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
@ -227,23 +220,40 @@ class WorkSpaceAvailabilityCheckEndpoint(BaseAPIView):
|
|||||||
return Response({"status": not workspace}, status=status.HTTP_200_OK)
|
return Response({"status": not workspace}, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
class InviteWorkspaceEndpoint(BaseAPIView):
|
class WorkspaceInvitationsViewset(BaseViewSet):
|
||||||
|
"""Endpoint for creating, listing and deleting workspaces"""
|
||||||
|
|
||||||
|
serializer_class = WorkSpaceMemberInviteSerializer
|
||||||
|
model = WorkspaceMemberInvite
|
||||||
|
|
||||||
permission_classes = [
|
permission_classes = [
|
||||||
WorkSpaceAdminPermission,
|
WorkSpaceAdminPermission,
|
||||||
]
|
]
|
||||||
|
|
||||||
def post(self, request, slug):
|
def get_queryset(self):
|
||||||
emails = request.data.get("emails", False)
|
return self.filter_queryset(
|
||||||
|
super()
|
||||||
|
.get_queryset()
|
||||||
|
.filter(workspace__slug=self.kwargs.get("slug"))
|
||||||
|
.select_related("workspace", "workspace__owner", "created_by")
|
||||||
|
)
|
||||||
|
|
||||||
|
def create(self, request, slug):
|
||||||
|
emails = request.data.get("emails", [])
|
||||||
# Check if email is provided
|
# Check if email is provided
|
||||||
if not emails or not len(emails):
|
if not emails:
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "Emails are required"}, status=status.HTTP_400_BAD_REQUEST
|
{"error": "Emails are required"}, status=status.HTTP_400_BAD_REQUEST
|
||||||
)
|
)
|
||||||
|
|
||||||
# check for role level
|
# check for role level of the requesting user
|
||||||
requesting_user = WorkspaceMember.objects.get(
|
requesting_user = WorkspaceMember.objects.get(
|
||||||
workspace__slug=slug, member=request.user
|
workspace__slug=slug,
|
||||||
|
member=request.user,
|
||||||
|
is_active=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Check if any invited user has an higher role
|
||||||
if len(
|
if len(
|
||||||
[
|
[
|
||||||
email
|
email
|
||||||
@ -256,15 +266,17 @@ class InviteWorkspaceEndpoint(BaseAPIView):
|
|||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Get the workspace object
|
||||||
workspace = Workspace.objects.get(slug=slug)
|
workspace = Workspace.objects.get(slug=slug)
|
||||||
|
|
||||||
# Check if user is already a member of workspace
|
# Check if user is already a member of workspace
|
||||||
workspace_members = WorkspaceMember.objects.filter(
|
workspace_members = WorkspaceMember.objects.filter(
|
||||||
workspace_id=workspace.id,
|
workspace_id=workspace.id,
|
||||||
member__email__in=[email.get("email") for email in emails],
|
member__email__in=[email.get("email") for email in emails],
|
||||||
|
is_active=True,
|
||||||
).select_related("member", "workspace", "workspace__owner")
|
).select_related("member", "workspace", "workspace__owner")
|
||||||
|
|
||||||
if len(workspace_members):
|
if workspace_members:
|
||||||
return Response(
|
return Response(
|
||||||
{
|
{
|
||||||
"error": "Some users are already member of workspace",
|
"error": "Some users are already member of workspace",
|
||||||
@ -302,35 +314,20 @@ class InviteWorkspaceEndpoint(BaseAPIView):
|
|||||||
},
|
},
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
WorkspaceMemberInvite.objects.bulk_create(
|
# Create workspace member invite
|
||||||
|
workspace_invitations = WorkspaceMemberInvite.objects.bulk_create(
|
||||||
workspace_invitations, batch_size=10, ignore_conflicts=True
|
workspace_invitations, batch_size=10, ignore_conflicts=True
|
||||||
)
|
)
|
||||||
|
|
||||||
workspace_invitations = WorkspaceMemberInvite.objects.filter(
|
current_site = f"{request.scheme}://{request.get_host()}",
|
||||||
email__in=[email.get("email") for email in emails]
|
|
||||||
).select_related("workspace")
|
|
||||||
|
|
||||||
# create the user if signup is disabled
|
|
||||||
if settings.DOCKERIZED and not settings.ENABLE_SIGNUP:
|
|
||||||
_ = User.objects.bulk_create(
|
|
||||||
[
|
|
||||||
User(
|
|
||||||
username=str(uuid4().hex),
|
|
||||||
email=invitation.email,
|
|
||||||
password=make_password(uuid4().hex),
|
|
||||||
is_password_autoset=True,
|
|
||||||
)
|
|
||||||
for invitation in workspace_invitations
|
|
||||||
],
|
|
||||||
batch_size=100,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
# Send invitations
|
||||||
for invitation in workspace_invitations:
|
for invitation in workspace_invitations:
|
||||||
workspace_invitation.delay(
|
workspace_invitation.delay(
|
||||||
invitation.email,
|
invitation.email,
|
||||||
workspace.id,
|
workspace.id,
|
||||||
invitation.token,
|
invitation.token,
|
||||||
request.META.get('HTTP_ORIGIN'),
|
current_site,
|
||||||
request.user.email,
|
request.user.email,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -341,11 +338,19 @@ class InviteWorkspaceEndpoint(BaseAPIView):
|
|||||||
status=status.HTTP_200_OK,
|
status=status.HTTP_200_OK,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def destroy(self, request, slug, pk):
|
||||||
|
workspace_member_invite = WorkspaceMemberInvite.objects.get(
|
||||||
|
pk=pk, workspace__slug=slug
|
||||||
|
)
|
||||||
|
workspace_member_invite.delete()
|
||||||
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
|
||||||
class JoinWorkspaceEndpoint(BaseAPIView):
|
|
||||||
|
class WorkspaceJoinEndpoint(BaseAPIView):
|
||||||
permission_classes = [
|
permission_classes = [
|
||||||
AllowAny,
|
AllowAny,
|
||||||
]
|
]
|
||||||
|
"""Invitation response endpoint the user can respond to the invitation"""
|
||||||
|
|
||||||
def post(self, request, slug, pk):
|
def post(self, request, slug, pk):
|
||||||
workspace_invite = WorkspaceMemberInvite.objects.get(
|
workspace_invite = WorkspaceMemberInvite.objects.get(
|
||||||
@ -354,12 +359,14 @@ class JoinWorkspaceEndpoint(BaseAPIView):
|
|||||||
|
|
||||||
email = request.data.get("email", "")
|
email = request.data.get("email", "")
|
||||||
|
|
||||||
|
# Check the email
|
||||||
if email == "" or workspace_invite.email != email:
|
if email == "" or workspace_invite.email != email:
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "You do not have permission to join the workspace"},
|
{"error": "You do not have permission to join the workspace"},
|
||||||
status=status.HTTP_403_FORBIDDEN,
|
status=status.HTTP_403_FORBIDDEN,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# If already responded then return error
|
||||||
if workspace_invite.responded_at is None:
|
if workspace_invite.responded_at is None:
|
||||||
workspace_invite.accepted = request.data.get("accepted", False)
|
workspace_invite.accepted = request.data.get("accepted", False)
|
||||||
workspace_invite.responded_at = timezone.now()
|
workspace_invite.responded_at = timezone.now()
|
||||||
@ -371,12 +378,23 @@ class JoinWorkspaceEndpoint(BaseAPIView):
|
|||||||
|
|
||||||
# If the user is present then create the workspace member
|
# If the user is present then create the workspace member
|
||||||
if user is not None:
|
if user is not None:
|
||||||
WorkspaceMember.objects.create(
|
# Check if the user was already a member of workspace then activate the user
|
||||||
workspace=workspace_invite.workspace,
|
workspace_member = WorkspaceMember.objects.filter(
|
||||||
member=user,
|
workspace=workspace_invite.workspace, member=user
|
||||||
role=workspace_invite.role,
|
).first()
|
||||||
)
|
if workspace_member is not None:
|
||||||
|
workspace_member.is_active = True
|
||||||
|
workspace_member.role = workspace_invite.role
|
||||||
|
workspace_member.save()
|
||||||
|
else:
|
||||||
|
# Create a Workspace
|
||||||
|
_ = WorkspaceMember.objects.create(
|
||||||
|
workspace=workspace_invite.workspace,
|
||||||
|
member=user,
|
||||||
|
role=workspace_invite.role,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Set the user last_workspace_id to the accepted workspace
|
||||||
user.last_workspace_id = workspace_invite.workspace.id
|
user.last_workspace_id = workspace_invite.workspace.id
|
||||||
user.save()
|
user.save()
|
||||||
|
|
||||||
@ -388,6 +406,7 @@ class JoinWorkspaceEndpoint(BaseAPIView):
|
|||||||
status=status.HTTP_200_OK,
|
status=status.HTTP_200_OK,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Workspace invitation rejected
|
||||||
return Response(
|
return Response(
|
||||||
{"message": "Workspace Invitation was not accepted"},
|
{"message": "Workspace Invitation was not accepted"},
|
||||||
status=status.HTTP_200_OK,
|
status=status.HTTP_200_OK,
|
||||||
@ -398,37 +417,13 @@ class JoinWorkspaceEndpoint(BaseAPIView):
|
|||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def get(self, request, slug, pk):
|
||||||
class WorkspaceInvitationsViewset(BaseViewSet):
|
workspace_invitation = WorkspaceMemberInvite.objects.get(workspace__slug=slug, pk=pk)
|
||||||
serializer_class = WorkSpaceMemberInviteSerializer
|
serializer = WorkSpaceMemberInviteSerializer(workspace_invitation)
|
||||||
model = WorkspaceMemberInvite
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
permission_classes = [
|
|
||||||
WorkSpaceAdminPermission,
|
|
||||||
]
|
|
||||||
|
|
||||||
def get_queryset(self):
|
|
||||||
return self.filter_queryset(
|
|
||||||
super()
|
|
||||||
.get_queryset()
|
|
||||||
.filter(workspace__slug=self.kwargs.get("slug"))
|
|
||||||
.select_related("workspace", "workspace__owner", "created_by")
|
|
||||||
)
|
|
||||||
|
|
||||||
def destroy(self, request, slug, pk):
|
|
||||||
workspace_member_invite = WorkspaceMemberInvite.objects.get(
|
|
||||||
pk=pk, workspace__slug=slug
|
|
||||||
)
|
|
||||||
# delete the user if signup is disabled
|
|
||||||
if settings.DOCKERIZED and not settings.ENABLE_SIGNUP:
|
|
||||||
user = User.objects.filter(email=workspace_member_invite.email).first()
|
|
||||||
if user is not None:
|
|
||||||
user.delete()
|
|
||||||
workspace_member_invite.delete()
|
|
||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
|
||||||
|
|
||||||
|
|
||||||
class UserWorkspaceInvitationsEndpoint(BaseViewSet):
|
class UserWorkspaceInvitationsViewSet(BaseViewSet):
|
||||||
serializer_class = WorkSpaceMemberInviteSerializer
|
serializer_class = WorkSpaceMemberInviteSerializer
|
||||||
model = WorkspaceMemberInvite
|
model = WorkspaceMemberInvite
|
||||||
|
|
||||||
@ -442,9 +437,19 @@ class UserWorkspaceInvitationsEndpoint(BaseViewSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def create(self, request):
|
def create(self, request):
|
||||||
invitations = request.data.get("invitations")
|
invitations = request.data.get("invitations", [])
|
||||||
workspace_invitations = WorkspaceMemberInvite.objects.filter(pk__in=invitations)
|
workspace_invitations = WorkspaceMemberInvite.objects.filter(
|
||||||
|
pk__in=invitations, email=request.user.email
|
||||||
|
).order_by("-created_at")
|
||||||
|
|
||||||
|
# If the user is already a member of workspace and was deactivated then activate the user
|
||||||
|
for invitation in workspace_invitations:
|
||||||
|
# Update the WorkspaceMember for this specific invitation
|
||||||
|
WorkspaceMember.objects.filter(
|
||||||
|
workspace_id=invitation.workspace_id, member=request.user
|
||||||
|
).update(is_active=True, role=invitation.role)
|
||||||
|
|
||||||
|
# Bulk create the user for all the workspaces
|
||||||
WorkspaceMember.objects.bulk_create(
|
WorkspaceMember.objects.bulk_create(
|
||||||
[
|
[
|
||||||
WorkspaceMember(
|
WorkspaceMember(
|
||||||
@ -481,20 +486,24 @@ class WorkSpaceMemberViewSet(BaseViewSet):
|
|||||||
return self.filter_queryset(
|
return self.filter_queryset(
|
||||||
super()
|
super()
|
||||||
.get_queryset()
|
.get_queryset()
|
||||||
.filter(workspace__slug=self.kwargs.get("slug"), member__is_bot=False)
|
.filter(
|
||||||
|
workspace__slug=self.kwargs.get("slug"),
|
||||||
|
member__is_bot=False,
|
||||||
|
is_active=True,
|
||||||
|
)
|
||||||
.select_related("workspace", "workspace__owner")
|
.select_related("workspace", "workspace__owner")
|
||||||
.select_related("member")
|
.select_related("member")
|
||||||
)
|
)
|
||||||
|
|
||||||
def list(self, request, slug):
|
def list(self, request, slug):
|
||||||
workspace_member = WorkspaceMember.objects.get(
|
workspace_member = WorkspaceMember.objects.get(
|
||||||
member=request.user, workspace__slug=slug
|
member=request.user,
|
||||||
|
workspace__slug=slug,
|
||||||
|
is_active=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
workspace_members = WorkspaceMember.objects.filter(
|
# Get all active workspace members
|
||||||
workspace__slug=slug,
|
workspace_members = self.get_queryset()
|
||||||
member__is_bot=False,
|
|
||||||
).select_related("workspace", "member")
|
|
||||||
|
|
||||||
if workspace_member.role > 10:
|
if workspace_member.role > 10:
|
||||||
serializer = WorkspaceMemberAdminSerializer(workspace_members, many=True)
|
serializer = WorkspaceMemberAdminSerializer(workspace_members, many=True)
|
||||||
@ -506,7 +515,12 @@ class WorkSpaceMemberViewSet(BaseViewSet):
|
|||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
def partial_update(self, request, slug, pk):
|
def partial_update(self, request, slug, pk):
|
||||||
workspace_member = WorkspaceMember.objects.get(pk=pk, workspace__slug=slug)
|
workspace_member = WorkspaceMember.objects.get(
|
||||||
|
pk=pk,
|
||||||
|
workspace__slug=slug,
|
||||||
|
member__is_bot=False,
|
||||||
|
is_active=True,
|
||||||
|
)
|
||||||
if request.user.id == workspace_member.member_id:
|
if request.user.id == workspace_member.member_id:
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "You cannot update your own role"},
|
{"error": "You cannot update your own role"},
|
||||||
@ -515,7 +529,9 @@ class WorkSpaceMemberViewSet(BaseViewSet):
|
|||||||
|
|
||||||
# Get the requested user role
|
# Get the requested user role
|
||||||
requested_workspace_member = WorkspaceMember.objects.get(
|
requested_workspace_member = WorkspaceMember.objects.get(
|
||||||
workspace__slug=slug, member=request.user
|
workspace__slug=slug,
|
||||||
|
member=request.user,
|
||||||
|
is_active=True,
|
||||||
)
|
)
|
||||||
# Check if role is being updated
|
# Check if role is being updated
|
||||||
# One cannot update role higher than his own role
|
# One cannot update role higher than his own role
|
||||||
@ -540,68 +556,121 @@ class WorkSpaceMemberViewSet(BaseViewSet):
|
|||||||
|
|
||||||
def destroy(self, request, slug, pk):
|
def destroy(self, request, slug, pk):
|
||||||
# Check the user role who is deleting the user
|
# Check the user role who is deleting the user
|
||||||
workspace_member = WorkspaceMember.objects.get(workspace__slug=slug, pk=pk)
|
workspace_member = WorkspaceMember.objects.get(
|
||||||
|
workspace__slug=slug,
|
||||||
|
pk=pk,
|
||||||
|
member__is_bot=False,
|
||||||
|
is_active=True,
|
||||||
|
)
|
||||||
|
|
||||||
# check requesting user role
|
# check requesting user role
|
||||||
requesting_workspace_member = WorkspaceMember.objects.get(
|
requesting_workspace_member = WorkspaceMember.objects.get(
|
||||||
workspace__slug=slug, member=request.user
|
workspace__slug=slug,
|
||||||
|
member=request.user,
|
||||||
|
is_active=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if str(workspace_member.id) == str(requesting_workspace_member.id):
|
||||||
|
return Response(
|
||||||
|
{
|
||||||
|
"error": "You cannot remove yourself from the workspace. Please use leave workspace"
|
||||||
|
},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
if requesting_workspace_member.role < workspace_member.role:
|
if requesting_workspace_member.role < workspace_member.role:
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "You cannot remove a user having role higher than you"},
|
{"error": "You cannot remove a user having role higher than you"},
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Check for the only member in the workspace
|
|
||||||
if (
|
if (
|
||||||
workspace_member.role == 20
|
Project.objects.annotate(
|
||||||
and WorkspaceMember.objects.filter(
|
total_members=Count("project_projectmember"),
|
||||||
workspace__slug=slug,
|
member_with_role=Count(
|
||||||
role=20,
|
"project_projectmember",
|
||||||
member__is_bot=False,
|
filter=Q(
|
||||||
).count()
|
project_projectmember__member_id=request.user.id,
|
||||||
== 1
|
project_projectmember__role=20,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.filter(total_members=1, member_with_role=1, workspace__slug=slug)
|
||||||
|
.exists()
|
||||||
):
|
):
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "Cannot delete the only Admin for the workspace"},
|
{
|
||||||
|
"error": "User is part of some projects where they are the only admin you should leave that project first"
|
||||||
|
},
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Delete the user also from all the projects
|
# Deactivate the users from the projects where the user is part of
|
||||||
ProjectMember.objects.filter(
|
_ = ProjectMember.objects.filter(
|
||||||
workspace__slug=slug, member=workspace_member.member
|
workspace__slug=slug,
|
||||||
).delete()
|
member_id=workspace_member.member_id,
|
||||||
# Remove all favorites
|
is_active=True,
|
||||||
ProjectFavorite.objects.filter(
|
).update(is_active=False)
|
||||||
workspace__slug=slug, user=workspace_member.member
|
|
||||||
).delete()
|
|
||||||
CycleFavorite.objects.filter(
|
|
||||||
workspace__slug=slug, user=workspace_member.member
|
|
||||||
).delete()
|
|
||||||
ModuleFavorite.objects.filter(
|
|
||||||
workspace__slug=slug, user=workspace_member.member
|
|
||||||
).delete()
|
|
||||||
PageFavorite.objects.filter(
|
|
||||||
workspace__slug=slug, user=workspace_member.member
|
|
||||||
).delete()
|
|
||||||
IssueViewFavorite.objects.filter(
|
|
||||||
workspace__slug=slug, user=workspace_member.member
|
|
||||||
).delete()
|
|
||||||
# Also remove issue from issue assigned
|
|
||||||
IssueAssignee.objects.filter(
|
|
||||||
workspace__slug=slug, assignee=workspace_member.member
|
|
||||||
).delete()
|
|
||||||
|
|
||||||
# Remove if module member
|
workspace_member.is_active = False
|
||||||
ModuleMember.objects.filter(
|
workspace_member.save()
|
||||||
workspace__slug=slug, member=workspace_member.member
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
).delete()
|
|
||||||
# Delete owned Pages
|
|
||||||
Page.objects.filter(
|
|
||||||
workspace__slug=slug, owned_by=workspace_member.member
|
|
||||||
).delete()
|
|
||||||
|
|
||||||
workspace_member.delete()
|
def leave(self, request, slug):
|
||||||
|
workspace_member = WorkspaceMember.objects.get(
|
||||||
|
workspace__slug=slug,
|
||||||
|
member=request.user,
|
||||||
|
is_active=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check if the leaving user is the only admin of the workspace
|
||||||
|
if (
|
||||||
|
workspace_member.role == 20
|
||||||
|
and not WorkspaceMember.objects.filter(
|
||||||
|
workspace__slug=slug,
|
||||||
|
role=20,
|
||||||
|
is_active=True,
|
||||||
|
).count()
|
||||||
|
> 1
|
||||||
|
):
|
||||||
|
return Response(
|
||||||
|
{
|
||||||
|
"error": "You cannot leave the workspace as your the only admin of the workspace you will have to either delete the workspace or create an another admin"
|
||||||
|
},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
if (
|
||||||
|
Project.objects.annotate(
|
||||||
|
total_members=Count("project_projectmember"),
|
||||||
|
member_with_role=Count(
|
||||||
|
"project_projectmember",
|
||||||
|
filter=Q(
|
||||||
|
project_projectmember__member_id=request.user.id,
|
||||||
|
project_projectmember__role=20,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.filter(total_members=1, member_with_role=1, workspace__slug=slug)
|
||||||
|
.exists()
|
||||||
|
):
|
||||||
|
return Response(
|
||||||
|
{
|
||||||
|
"error": "User is part of some projects where they are the only admin you should leave that project first"
|
||||||
|
},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
# # Deactivate the users from the projects where the user is part of
|
||||||
|
_ = ProjectMember.objects.filter(
|
||||||
|
workspace__slug=slug,
|
||||||
|
member_id=workspace_member.member_id,
|
||||||
|
is_active=True,
|
||||||
|
).update(is_active=False)
|
||||||
|
|
||||||
|
# # Deactivate the user
|
||||||
|
workspace_member.is_active = False
|
||||||
|
workspace_member.save()
|
||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
|
||||||
|
|
||||||
@ -629,7 +698,9 @@ class TeamMemberViewSet(BaseViewSet):
|
|||||||
def create(self, request, slug):
|
def create(self, request, slug):
|
||||||
members = list(
|
members = list(
|
||||||
WorkspaceMember.objects.filter(
|
WorkspaceMember.objects.filter(
|
||||||
workspace__slug=slug, member__id__in=request.data.get("members", [])
|
workspace__slug=slug,
|
||||||
|
member__id__in=request.data.get("members", []),
|
||||||
|
is_active=True,
|
||||||
)
|
)
|
||||||
.annotate(member_str_id=Cast("member", output_field=CharField()))
|
.annotate(member_str_id=Cast("member", output_field=CharField()))
|
||||||
.distinct()
|
.distinct()
|
||||||
@ -658,23 +729,6 @@ class TeamMemberViewSet(BaseViewSet):
|
|||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
|
||||||
class UserWorkspaceInvitationEndpoint(BaseViewSet):
|
|
||||||
model = WorkspaceMemberInvite
|
|
||||||
serializer_class = WorkSpaceMemberInviteSerializer
|
|
||||||
|
|
||||||
permission_classes = [
|
|
||||||
AllowAny,
|
|
||||||
]
|
|
||||||
|
|
||||||
def get_queryset(self):
|
|
||||||
return self.filter_queryset(
|
|
||||||
super()
|
|
||||||
.get_queryset()
|
|
||||||
.filter(pk=self.kwargs.get("pk"))
|
|
||||||
.select_related("workspace")
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class UserLastProjectWithWorkspaceEndpoint(BaseAPIView):
|
class UserLastProjectWithWorkspaceEndpoint(BaseAPIView):
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
user = User.objects.get(pk=request.user.id)
|
user = User.objects.get(pk=request.user.id)
|
||||||
@ -711,7 +765,9 @@ class UserLastProjectWithWorkspaceEndpoint(BaseAPIView):
|
|||||||
class WorkspaceMemberUserEndpoint(BaseAPIView):
|
class WorkspaceMemberUserEndpoint(BaseAPIView):
|
||||||
def get(self, request, slug):
|
def get(self, request, slug):
|
||||||
workspace_member = WorkspaceMember.objects.get(
|
workspace_member = WorkspaceMember.objects.get(
|
||||||
member=request.user, workspace__slug=slug
|
member=request.user,
|
||||||
|
workspace__slug=slug,
|
||||||
|
is_active=True,
|
||||||
)
|
)
|
||||||
serializer = WorkspaceMemberMeSerializer(workspace_member)
|
serializer = WorkspaceMemberMeSerializer(workspace_member)
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
@ -720,7 +776,9 @@ class WorkspaceMemberUserEndpoint(BaseAPIView):
|
|||||||
class WorkspaceMemberUserViewsEndpoint(BaseAPIView):
|
class WorkspaceMemberUserViewsEndpoint(BaseAPIView):
|
||||||
def post(self, request, slug):
|
def post(self, request, slug):
|
||||||
workspace_member = WorkspaceMember.objects.get(
|
workspace_member = WorkspaceMember.objects.get(
|
||||||
workspace__slug=slug, member=request.user
|
workspace__slug=slug,
|
||||||
|
member=request.user,
|
||||||
|
is_active=True,
|
||||||
)
|
)
|
||||||
workspace_member.view_props = request.data.get("view_props", {})
|
workspace_member.view_props = request.data.get("view_props", {})
|
||||||
workspace_member.save()
|
workspace_member.save()
|
||||||
@ -1046,7 +1104,9 @@ class WorkspaceUserProfileEndpoint(BaseAPIView):
|
|||||||
user_data = User.objects.get(pk=user_id)
|
user_data = User.objects.get(pk=user_id)
|
||||||
|
|
||||||
requesting_workspace_member = WorkspaceMember.objects.get(
|
requesting_workspace_member = WorkspaceMember.objects.get(
|
||||||
workspace__slug=slug, member=request.user
|
workspace__slug=slug,
|
||||||
|
member=request.user,
|
||||||
|
is_active=True,
|
||||||
)
|
)
|
||||||
projects = []
|
projects = []
|
||||||
if requesting_workspace_member.role >= 10:
|
if requesting_workspace_member.role >= 10:
|
||||||
@ -1250,9 +1310,7 @@ class WorkspaceUserProfileIssuesEndpoint(BaseAPIView):
|
|||||||
status=status.HTTP_200_OK,
|
status=status.HTTP_200_OK,
|
||||||
)
|
)
|
||||||
|
|
||||||
return Response(
|
return Response(issues, status=status.HTTP_200_OK)
|
||||||
issues, status=status.HTTP_200_OK
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class WorkspaceLabelsEndpoint(BaseAPIView):
|
class WorkspaceLabelsEndpoint(BaseAPIView):
|
||||||
@ -1266,30 +1324,3 @@ class WorkspaceLabelsEndpoint(BaseAPIView):
|
|||||||
project__project_projectmember__member=request.user,
|
project__project_projectmember__member=request.user,
|
||||||
).values("parent", "name", "color", "id", "project_id", "workspace__slug")
|
).values("parent", "name", "color", "id", "project_id", "workspace__slug")
|
||||||
return Response(labels, status=status.HTTP_200_OK)
|
return Response(labels, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
class LeaveWorkspaceEndpoint(BaseAPIView):
|
|
||||||
permission_classes = [
|
|
||||||
WorkspaceEntityPermission,
|
|
||||||
]
|
|
||||||
|
|
||||||
def delete(self, request, slug):
|
|
||||||
workspace_member = WorkspaceMember.objects.get(
|
|
||||||
workspace__slug=slug, member=request.user
|
|
||||||
)
|
|
||||||
|
|
||||||
# Only Admin case
|
|
||||||
if (
|
|
||||||
workspace_member.role == 20
|
|
||||||
and WorkspaceMember.objects.filter(workspace__slug=slug, role=20).count()
|
|
||||||
== 1
|
|
||||||
):
|
|
||||||
return Response(
|
|
||||||
{
|
|
||||||
"error": "You cannot leave the workspace since you are the only admin of the workspace you should delete the workspace"
|
|
||||||
},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
# Delete the member from workspace
|
|
||||||
workspace_member.delete()
|
|
||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
|
||||||
|
@ -73,6 +73,12 @@ def service_importer(service, importer_id):
|
|||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Check if any of the users are already member of workspace
|
||||||
|
_ = WorkspaceMember.objects.filter(
|
||||||
|
member__in=[user for user in workspace_users],
|
||||||
|
workspace_id=importer.workspace_id,
|
||||||
|
).update(is_active=True)
|
||||||
|
|
||||||
# Add new users to Workspace and project automatically
|
# Add new users to Workspace and project automatically
|
||||||
WorkspaceMember.objects.bulk_create(
|
WorkspaceMember.objects.bulk_create(
|
||||||
[
|
[
|
||||||
|
@ -13,23 +13,24 @@ from plane.db.models import Project, User, ProjectMemberInvite
|
|||||||
|
|
||||||
|
|
||||||
@shared_task
|
@shared_task
|
||||||
def project_invitation(email, project_id, token, current_site):
|
def project_invitation(email, project_id, token, current_site, invitor):
|
||||||
try:
|
try:
|
||||||
|
user = User.objects.get(email=invitor)
|
||||||
project = Project.objects.get(pk=project_id)
|
project = Project.objects.get(pk=project_id)
|
||||||
project_member_invite = ProjectMemberInvite.objects.get(
|
project_member_invite = ProjectMemberInvite.objects.get(
|
||||||
token=token, email=email
|
token=token, email=email
|
||||||
)
|
)
|
||||||
|
|
||||||
relativelink = f"/project-member-invitation/{project_member_invite.id}"
|
relativelink = f"/project-invitations/?invitation_id={project_member_invite.id}&email={email}&slug={project.workspace.slug}&project_id={str(project_id)}"
|
||||||
abs_url = current_site + relativelink
|
abs_url = current_site + relativelink
|
||||||
|
|
||||||
from_email_string = settings.EMAIL_FROM
|
from_email_string = settings.EMAIL_FROM
|
||||||
|
|
||||||
subject = f"{project.created_by.first_name or project.created_by.email} invited you to join {project.name} on Plane"
|
subject = f"{user.first_name or user.display_name or user.email} invited you to join {project.name} on Plane"
|
||||||
|
|
||||||
context = {
|
context = {
|
||||||
"email": email,
|
"email": email,
|
||||||
"first_name": project.created_by.first_name,
|
"first_name": user.first_name,
|
||||||
"project_name": project.name,
|
"project_name": project.name,
|
||||||
"invitation_url": abs_url,
|
"invitation_url": abs_url,
|
||||||
}
|
}
|
||||||
|
@ -11,25 +11,33 @@ from slack_sdk import WebClient
|
|||||||
from slack_sdk.errors import SlackApiError
|
from slack_sdk.errors import SlackApiError
|
||||||
|
|
||||||
# Module imports
|
# Module imports
|
||||||
from plane.db.models import Workspace, WorkspaceMemberInvite
|
from plane.db.models import User, Workspace, WorkspaceMemberInvite
|
||||||
|
|
||||||
|
|
||||||
@shared_task
|
@shared_task
|
||||||
def workspace_invitation(email, workspace_id, token, current_site, invitor):
|
def workspace_invitation(email, workspace_id, token, current_site, invitor):
|
||||||
try:
|
try:
|
||||||
|
|
||||||
|
user = User.objects.get(email=invitor)
|
||||||
|
|
||||||
workspace = Workspace.objects.get(pk=workspace_id)
|
workspace = Workspace.objects.get(pk=workspace_id)
|
||||||
workspace_member_invite = WorkspaceMemberInvite.objects.get(
|
workspace_member_invite = WorkspaceMemberInvite.objects.get(
|
||||||
token=token, email=email
|
token=token, email=email
|
||||||
)
|
)
|
||||||
|
|
||||||
realtivelink = (
|
# Relative link
|
||||||
f"/workspace-member-invitation/?invitation_id={workspace_member_invite.id}&email={email}"
|
relative_link = (
|
||||||
|
f"/workspace-invitations/?invitation_id={workspace_member_invite.id}&email={email}&slug={workspace.slug}"
|
||||||
)
|
)
|
||||||
abs_url = current_site + realtivelink
|
|
||||||
|
|
||||||
|
# The complete url including the domain
|
||||||
|
abs_url = current_site + relative_link
|
||||||
|
|
||||||
|
# The email from
|
||||||
from_email_string = settings.EMAIL_FROM
|
from_email_string = settings.EMAIL_FROM
|
||||||
|
|
||||||
subject = f"{invitor or email} invited you to join {workspace.name} on Plane"
|
# Subject of the email
|
||||||
|
subject = f"{user.first_name or user.display_name or user.email} invited you to join {workspace.name} on Plane"
|
||||||
|
|
||||||
context = {
|
context = {
|
||||||
"email": email,
|
"email": email,
|
||||||
|
@ -0,0 +1,26 @@
|
|||||||
|
# Generated by Django 4.2.5 on 2023-11-09 11:19
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('db', '0046_alter_analyticview_created_by_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='projectmember',
|
||||||
|
name='is_deactivated',
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='workspacemember',
|
||||||
|
name='is_deactivated',
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
]
|
@ -0,0 +1,26 @@
|
|||||||
|
# Generated by Django 4.2.5 on 2023-11-10 09:41
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('db', '0047_issuemention_projectmember_is_deactivated_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='projectmember',
|
||||||
|
name='is_active',
|
||||||
|
field=models.BooleanField(default=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='workspacemember',
|
||||||
|
name='is_active',
|
||||||
|
field=models.BooleanField(default=True),
|
||||||
|
),
|
||||||
|
]
|
@ -0,0 +1,24 @@
|
|||||||
|
# Generated by Django 4.2.5 on 2023-11-11 17:57
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('db', '0048_issuemention_remove_projectmember_is_deactivated_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='projectmember',
|
||||||
|
name='is_deactivated',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='workspacemember',
|
||||||
|
name='is_deactivated',
|
||||||
|
),
|
||||||
|
]
|
@ -166,6 +166,7 @@ class ProjectMember(ProjectBaseModel):
|
|||||||
default_props = models.JSONField(default=get_default_props)
|
default_props = models.JSONField(default=get_default_props)
|
||||||
preferences = models.JSONField(default=get_default_preferences)
|
preferences = models.JSONField(default=get_default_preferences)
|
||||||
sort_order = models.FloatField(default=65535)
|
sort_order = models.FloatField(default=65535)
|
||||||
|
is_active = models.BooleanField(default=True)
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
if self._state.adding:
|
if self._state.adding:
|
||||||
|
@ -99,6 +99,7 @@ class WorkspaceMember(BaseModel):
|
|||||||
view_props = models.JSONField(default=get_default_props)
|
view_props = models.JSONField(default=get_default_props)
|
||||||
default_props = models.JSONField(default=get_default_props)
|
default_props = models.JSONField(default=get_default_props)
|
||||||
issue_props = models.JSONField(default=get_issue_props)
|
issue_props = models.JSONField(default=get_issue_props)
|
||||||
|
is_active = models.BooleanField(default=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
unique_together = ["workspace", "member"]
|
unique_together = ["workspace", "member"]
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
<meta name="format-detection" content="telephone=no">
|
<meta name="format-detection" content="telephone=no">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>{{ Inviter }} invited you to join {{ Workspace-Name }} on Plane</title>
|
<title>{{ first_name }} invited you to join {{ project_name }} on Plane</title>
|
||||||
<style type="text/css" emogrify="no">#outlook a { padding:0; } .ExternalClass { width:100%; } .ExternalClass, .ExternalClass p, .ExternalClass span, .ExternalClass font, .ExternalClass td, .ExternalClass div { line-height: 100%; } table td { border-collapse: collapse; mso-line-height-rule: exactly; } .editable.image { font-size: 0 !important; line-height: 0 !important; } .nl2go_preheader { display: none !important; mso-hide:all !important; mso-line-height-rule: exactly; visibility: hidden !important; line-height: 0px !important; font-size: 0px !important; } body { width:100% !important; -webkit-text-size-adjust:100%; -ms-text-size-adjust:100%; margin:0; padding:0; } img { outline:none; text-decoration:none; -ms-interpolation-mode: bicubic; } a img { border:none; } table { border-collapse:collapse; mso-table-lspace:0pt; mso-table-rspace:0pt; } th { font-weight: normal; text-align: left; } *[class="gmail-fix"] { display: none !important; } </style>
|
<style type="text/css" emogrify="no">#outlook a { padding:0; } .ExternalClass { width:100%; } .ExternalClass, .ExternalClass p, .ExternalClass span, .ExternalClass font, .ExternalClass td, .ExternalClass div { line-height: 100%; } table td { border-collapse: collapse; mso-line-height-rule: exactly; } .editable.image { font-size: 0 !important; line-height: 0 !important; } .nl2go_preheader { display: none !important; mso-hide:all !important; mso-line-height-rule: exactly; visibility: hidden !important; line-height: 0px !important; font-size: 0px !important; } body { width:100% !important; -webkit-text-size-adjust:100%; -ms-text-size-adjust:100%; margin:0; padding:0; } img { outline:none; text-decoration:none; -ms-interpolation-mode: bicubic; } a img { border:none; } table { border-collapse:collapse; mso-table-lspace:0pt; mso-table-rspace:0pt; } th { font-weight: normal; text-align: left; } *[class="gmail-fix"] { display: none !important; } </style>
|
||||||
<style type="text/css" emogrify="no"> @media (max-width: 600px) { .gmx-killpill { content: ' \03D1';} } </style>
|
<style type="text/css" emogrify="no"> @media (max-width: 600px) { .gmx-killpill { content: ' \03D1';} } </style>
|
||||||
<style type="text/css" emogrify="no">@media (max-width: 600px) { .gmx-killpill { content: ' \03D1';} .r0-c { box-sizing: border-box !important; text-align: center !important; valign: top !important; width: 320px !important } .r1-o { border-style: solid !important; margin: 0 auto 0 auto !important; width: 320px !important } .r2-i { background-color: #ffffff !important } .r3-c { box-sizing: border-box !important; text-align: center !important; valign: top !important; width: 100% !important } .r4-o { border-style: solid !important; margin: 0 auto 0 auto !important; margin-top: 20px !important; width: 100% !important } .r5-i { background-color: #f8f9fa !important; padding-bottom: 20px !important; padding-left: 10px !important; padding-right: 10px !important; padding-top: 20px !important } .r6-c { box-sizing: border-box !important; display: block !important; valign: top !important; width: 100% !important } .r7-o { border-style: solid !important; width: 100% !important } .r8-i { padding-left: 0px !important; padding-right: 0px !important } .r9-o { border-style: solid !important; margin: 0 auto 0 auto !important; width: 100% !important } .r10-i { padding-bottom: 35px !important; padding-top: 15px !important } .r11-c { box-sizing: border-box !important; text-align: left !important; valign: top !important; width: 100% !important } .r12-o { border-style: solid !important; margin: 0 auto 0 0 !important; width: 100% !important } .r13-i { padding-left: 20px !important; padding-right: 20px !important; padding-top: 0px !important; text-align: center !important } .r14-o { border-style: solid !important; margin: 0 auto 0 auto !important; margin-bottom: 20px !important; margin-top: 20px !important; width: 100% !important } .r15-i { text-align: center !important } .r16-r { background-color: #ffffff !important; border-color: #3f76ff !important; border-radius: 4px !important; border-width: 1px !important; box-sizing: border-box; height: initial !important; padding-bottom: 7px !important; padding-left: 20px !important; padding-right: 20px !important; padding-top: 7px !important; text-align: center !important; width: 100% !important } .r17-i { padding-bottom: 15px !important; padding-left: 20px !important; padding-right: 20px !important; padding-top: 15px !important; text-align: left !important } .r18-i { background-color: #eff2f7 !important; padding-bottom: 20px !important; padding-left: 15px !important; padding-right: 15px !important; padding-top: 20px !important } .r19-i { padding-bottom: 15px !important; padding-top: 15px !important } .r20-i { color: #3b3f44 !important; padding-bottom: 0px !important; padding-top: 0px !important; text-align: center !important } .r21-c { box-sizing: border-box !important; text-align: center !important; width: 100% !important } .r22-c { box-sizing: border-box !important; width: 100% !important } .r23-i { font-size: 0px !important; padding-bottom: 15px !important; padding-left: 65px !important; padding-right: 65px !important; padding-top: 15px !important } .r24-c { box-sizing: border-box !important; width: 32px !important } .r25-o { border-style: solid !important; margin-right: 8px !important; width: 32px !important } .r26-i { padding-bottom: 5px !important; padding-top: 5px !important } .r27-o { border-style: solid !important; margin-right: 0px !important; width: 32px !important } .r28-i { color: #3b3f44 !important; padding-bottom: 15px !important; padding-top: 15px !important; text-align: center !important } .r29-i { padding-bottom: 15px !important; padding-left: 0px !important; padding-right: 0px !important; padding-top: 0px !important } .r30-c { box-sizing: border-box !important; text-align: center !important; valign: top !important; width: 129px !important } .r31-o { border-style: solid !important; margin: 0 auto 0 auto !important; width: 129px !important } body { -webkit-text-size-adjust: none } .nl2go-responsive-hide { display: none } .nl2go-body-table { min-width: unset !important } .mobshow { height: auto !important; overflow: visible !important; max-height: unset !important; visibility: visible !important; border: none !important } .resp-table { display: inline-table !important } .magic-resp { display: table-cell !important } } </style>
|
<style type="text/css" emogrify="no">@media (max-width: 600px) { .gmx-killpill { content: ' \03D1';} .r0-c { box-sizing: border-box !important; text-align: center !important; valign: top !important; width: 320px !important } .r1-o { border-style: solid !important; margin: 0 auto 0 auto !important; width: 320px !important } .r2-i { background-color: #ffffff !important } .r3-c { box-sizing: border-box !important; text-align: center !important; valign: top !important; width: 100% !important } .r4-o { border-style: solid !important; margin: 0 auto 0 auto !important; margin-top: 20px !important; width: 100% !important } .r5-i { background-color: #f8f9fa !important; padding-bottom: 20px !important; padding-left: 10px !important; padding-right: 10px !important; padding-top: 20px !important } .r6-c { box-sizing: border-box !important; display: block !important; valign: top !important; width: 100% !important } .r7-o { border-style: solid !important; width: 100% !important } .r8-i { padding-left: 0px !important; padding-right: 0px !important } .r9-o { border-style: solid !important; margin: 0 auto 0 auto !important; width: 100% !important } .r10-i { padding-bottom: 35px !important; padding-top: 15px !important } .r11-c { box-sizing: border-box !important; text-align: left !important; valign: top !important; width: 100% !important } .r12-o { border-style: solid !important; margin: 0 auto 0 0 !important; width: 100% !important } .r13-i { padding-left: 20px !important; padding-right: 20px !important; padding-top: 0px !important; text-align: center !important } .r14-o { border-style: solid !important; margin: 0 auto 0 auto !important; margin-bottom: 20px !important; margin-top: 20px !important; width: 100% !important } .r15-i { text-align: center !important } .r16-r { background-color: #ffffff !important; border-color: #3f76ff !important; border-radius: 4px !important; border-width: 1px !important; box-sizing: border-box; height: initial !important; padding-bottom: 7px !important; padding-left: 20px !important; padding-right: 20px !important; padding-top: 7px !important; text-align: center !important; width: 100% !important } .r17-i { padding-bottom: 15px !important; padding-left: 20px !important; padding-right: 20px !important; padding-top: 15px !important; text-align: left !important } .r18-i { background-color: #eff2f7 !important; padding-bottom: 20px !important; padding-left: 15px !important; padding-right: 15px !important; padding-top: 20px !important } .r19-i { padding-bottom: 15px !important; padding-top: 15px !important } .r20-i { color: #3b3f44 !important; padding-bottom: 0px !important; padding-top: 0px !important; text-align: center !important } .r21-c { box-sizing: border-box !important; text-align: center !important; width: 100% !important } .r22-c { box-sizing: border-box !important; width: 100% !important } .r23-i { font-size: 0px !important; padding-bottom: 15px !important; padding-left: 65px !important; padding-right: 65px !important; padding-top: 15px !important } .r24-c { box-sizing: border-box !important; width: 32px !important } .r25-o { border-style: solid !important; margin-right: 8px !important; width: 32px !important } .r26-i { padding-bottom: 5px !important; padding-top: 5px !important } .r27-o { border-style: solid !important; margin-right: 0px !important; width: 32px !important } .r28-i { color: #3b3f44 !important; padding-bottom: 15px !important; padding-top: 15px !important; text-align: center !important } .r29-i { padding-bottom: 15px !important; padding-left: 0px !important; padding-right: 0px !important; padding-top: 0px !important } .r30-c { box-sizing: border-box !important; text-align: center !important; valign: top !important; width: 129px !important } .r31-o { border-style: solid !important; margin: 0 auto 0 auto !important; width: 129px !important } body { -webkit-text-size-adjust: none } .nl2go-responsive-hide { display: none } .nl2go-body-table { min-width: unset !important } .mobshow { height: auto !important; overflow: visible !important; max-height: unset !important; visibility: visible !important; border: none !important } .resp-table { display: inline-table !important } .magic-resp { display: table-cell !important } } </style>
|
||||||
|
Loading…
Reference in New Issue
Block a user