diff --git a/apps/project/admin.py b/apps/project/admin.py index 084c784e2e..5c43910db7 100644 --- a/apps/project/admin.py +++ b/apps/project/admin.py @@ -24,9 +24,8 @@ ProjectJoinRequest, ProjectOrganization, ProjectChangeLog, - ProjectPinned + ProjectPinned, ) - TRIGGER_LIMIT = 5 @@ -221,7 +220,20 @@ class ProjectMembershipAdmin(admin.ModelAdmin): list_filter = ( AutocompleteFilterFactory('Project', 'project'), ) - list_display = ['project', 'member'] + list_display = ['project', 'member', 'role', 'added_by'] + + def get_readonly_fields(self, request, obj=None): + # editing an existing object + if obj: + return self.readonly_fields + ('project', ) + return self.readonly_fields + + +@admin.register(ProjectJoinRequest) +class ProjectJoinAdmin(admin.ModelAdmin): + search_fields = ['project__title'] + autocomplete_fields = ('requested_by', 'responded_by', 'project') + list_display = ['project', 'requested_by', 'responded_by', 'status'] def get_readonly_fields(self, request, obj=None): # editing an existing object diff --git a/apps/project/mutation.py b/apps/project/mutation.py index 506affa4f2..9362f645f2 100644 --- a/apps/project/mutation.py +++ b/apps/project/mutation.py @@ -13,6 +13,7 @@ PsGrapheneMutation, PsBulkGrapheneMutation, DeleteMutation, + ProjectScopeMixin ) from utils.graphene.error_types import mutation_is_not_valid, CustomErrorType @@ -48,12 +49,14 @@ ProjectUserGroupMembershipGqlSerializer as ProjectUserGroupMembershipSerializer, ProjectVizConfigurationSerializer, UserPinnedProjectSerializer, - BulkProjectPinnedSerializer + BulkProjectPinnedSerializer, + UserProjectLeaveSerializer ) from .schema import ( ProjectDetailType, ProjectJoinRequestType, ProjectMembershipType, + ProjectType, ProjectUserGroupMembershipType, ProjectVizDataType, UserPinnedProjectType @@ -106,6 +109,11 @@ serializer_class=BulkProjectPinnedSerializer, ) +UserProjectLeaveInputType = generate_input_type_for_serializer( + 'UserProjectLeaveInputType', + serializer_class=UserProjectLeaveSerializer, +) + class CreateProject(GrapheneMutation): class Arguments: @@ -340,6 +348,23 @@ class Arguments: permissions = [] +class LeaveProject(ProjectScopeMixin, graphene.Mutation): + ok = graphene.Boolean() + result = graphene.Field(ProjectType) + errors = graphene.List(graphene.NonNull(CustomErrorType)) + serializer_class = UserProjectLeaveSerializer + permissions = [] + + @classmethod + def mutate(cls, root, info, **kwargs): + project = info.context.active_project + serializer = UserProjectLeaveSerializer(data={}, context={'request': info.context.request}) + if errors := mutation_is_not_valid(serializer): + return LeaveProject(errors=errors, ok=False) + serializer.save() + return LeaveProject(result=project, errors=None, ok=True) + + class ProjectMutationType( # --Begin Project Scoped Mutation LeadMutation, @@ -369,6 +394,7 @@ class Meta: project_viz_configuration_update = UpdateProjectVizConfiguration.Field() unified_connector = graphene.Field(UnifiedConnectorMutationType) assisted_tagging = graphene.Field(AssistedTaggingMutationType) + leave_project = LeaveProject.Field() @staticmethod def get_custom_node(_, info, id): diff --git a/apps/project/serializers.py b/apps/project/serializers.py index 4e50d4a555..c1bc745163 100644 --- a/apps/project/serializers.py +++ b/apps/project/serializers.py @@ -959,3 +959,27 @@ def create(self, validated_data): def update(self, instance, validated_data): return super().update(instance, validated_data) + + +class UserProjectLeaveSerializer(ProjectPropertySerializerMixin, serializers.Serializer): + + def validate(self, data): + membership = ProjectMembership.objects.filter( + project=self.project, + ) + if membership.count() == 1: + raise serializers.ValidationError('Last member of project can\'t leave') + + owner_memberships = membership.filter(role=ProjectRole.get_owner_role()) + if ( + owner_memberships.count() == 1 and + owner_memberships.filter(member=self.current_user) + ): + raise serializers.ValidationError('Last owner of project can\'t leave') + return data + + def create(self, data): + ProjectJoinRequest.objects.filter(project=self.project, requested_by=self.current_user).delete() + ProjectPinned.objects.filter(project=self.project, user=self.current_user).delete() + ProjectMembership.objects.filter(project=self.project, member=self.current_user).delete() + return data diff --git a/schema.graphql b/schema.graphql index e1ba512bcd..ac3808e7e1 100644 --- a/schema.graphql +++ b/schema.graphql @@ -4980,6 +4980,12 @@ type LeadsFilterDataType { duplicatesOf: ID } +type LeaveProject { + ok: Boolean + result: ProjectType + errors: [GenericScalar!] +} + type Login { result: UserMeType errors: [GenericScalar!] @@ -5552,6 +5558,7 @@ type ProjectMutationType { projectVizConfigurationUpdate(data: ProjectVizConfigurationInputType!): UpdateProjectVizConfiguration unifiedConnector: UnifiedConnectorMutationType assistedTagging: AssistedTaggingMutationType + leaveProject: LeaveProject } enum ProjectOrderingEnum {