From c9fc2268d036b7c10350810d2ac332a63ede02ba Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Thu, 20 Nov 2014 09:26:46 +0100 Subject: [PATCH] Adding leave project API --- taiga/projects/api.py | 7 +++++++ taiga/projects/permissions.py | 30 +++++++++++++++++++++++++-- taiga/projects/services/__init__.py | 1 + taiga/projects/services/members.py | 5 ++++- tests/integration/test_projects.py | 32 +++++++++++++++++++++++++++++ 5 files changed, 72 insertions(+), 3 deletions(-) diff --git a/taiga/projects/api.py b/taiga/projects/api.py index 19ba5ffd..812ac6b0 100644 --- a/taiga/projects/api.py +++ b/taiga/projects/api.py @@ -154,6 +154,13 @@ class ProjectViewSet(ModelCrudViewSet): template.save() return Response(serializers.ProjectTemplateSerializer(template).data, status=201) + @detail_route(methods=['post']) + def leave(self, request, pk=None): + project = self.get_object() + self.check_permissions(request, 'leave', project) + services.remove_member(project, user=request.user) + return Response(status=status.HTTP_200_OK) + def pre_save(self, obj): if not obj.id: obj.owner = self.request.user diff --git a/taiga/projects/permissions.py b/taiga/projects/permissions.py index 288b2aea..e8e476e8 100644 --- a/taiga/projects/permissions.py +++ b/taiga/projects/permissions.py @@ -13,10 +13,35 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +from django.utils.translation import ugettext_lazy as _ from taiga.base.api.permissions import (TaigaResourcePermission, HasProjectPerm, IsAuthenticated, IsProjectOwner, - AllowAny, IsSuperUser) + AllowAny, IsSuperUser, PermissionComponent) + +from taiga.base import exceptions as exc +from taiga.projects.models import Membership + + +class CanLeaveProject(PermissionComponent): + def check_permissions(self, request, view, obj=None): + if not obj or not request.user.is_authenticated(): + return False + + try: + membership = Membership.objects.get(user=request.user, project=obj) + other_admin_memberships_count = Membership.objects\ + .exclude(id=membership.id)\ + .filter(project=obj, is_owner=True)\ + .count() + + # The project need at least one owner + if membership.is_owner and other_admin_memberships_count == 0: + raise exc.PermissionDenied(_("You can't leave the project if there are no more owners")) + + return True + except Membership.DoesNotExist: + return False class ProjectPermission(TaigaResourcePermission): @@ -28,7 +53,7 @@ class ProjectPermission(TaigaResourcePermission): modules_perms = IsProjectOwner() list_perms = AllowAny() stats_perms = AllowAny() - member_stats_perms = HasProjectPerm('view_project') + member_stats_perms = HasProjectPerm('view_project') star_perms = IsAuthenticated() unstar_perms = IsAuthenticated() issues_stats_perms = AllowAny() @@ -37,6 +62,7 @@ class ProjectPermission(TaigaResourcePermission): tags_colors_perms = HasProjectPerm('view_project') fans_perms = HasProjectPerm('view_project') create_template_perms = IsSuperUser() + leave_perms = CanLeaveProject() class MembershipPermission(TaigaResourcePermission): diff --git a/taiga/projects/services/__init__.py b/taiga/projects/services/__init__.py index 06e47b54..e4a98aa0 100644 --- a/taiga/projects/services/__init__.py +++ b/taiga/projects/services/__init__.py @@ -34,6 +34,7 @@ from .stats import get_member_stats_for_project from .members import create_members_in_bulk from .members import get_members_from_bulk +from .members import remove_member from .invitations import send_invitation from .invitations import find_invited_user diff --git a/taiga/projects/services/members.py b/taiga/projects/services/members.py index 0758cd66..58f77670 100644 --- a/taiga/projects/services/members.py +++ b/taiga/projects/services/members.py @@ -2,7 +2,6 @@ from taiga.base.utils import db, text from .. import models - def get_members_from_bulk(bulk_data, **additional_fields): """Convert `bulk_data` into a list of members. @@ -31,3 +30,7 @@ def create_members_in_bulk(bulk_data, callback=None, precall=None, **additional_ members = get_members_from_bulk(bulk_data, **additional_fields) db.save_in_bulk(members, callback, precall) return members + + +def remove_member(project, user): + models.Membership.objects.get(project=project, user=user).delete() diff --git a/tests/integration/test_projects.py b/tests/integration/test_projects.py index 4667b124..26607bf5 100644 --- a/tests/integration/test_projects.py +++ b/tests/integration/test_projects.py @@ -160,3 +160,35 @@ def test_get_closed_bugs_per_member_stats(): assert stats["closed_tasks"][membership_1.user.id] == 1 assert stats["closed_tasks"][membership_2.user.id] == 0 + + +def test_leave_project_valid_membership(client): + user = f.UserFactory.create() + project = f.ProjectFactory.create() + role = f.RoleFactory.create(project=project, permissions=["view_project"]) + f.MembershipFactory.create(project=project, user=user, role=role) + client.login(user) + url = reverse("projects-leave", args=(project.id,)) + response = client.post(url) + assert response.status_code == 200 + + +def test_leave_project_valid_membership_only_owner(client): + user = f.UserFactory.create() + project = f.ProjectFactory.create() + role = f.RoleFactory.create(project=project, permissions=["view_project"]) + f.MembershipFactory.create(project=project, user=user, role=role, is_owner=True) + client.login(user) + url = reverse("projects-leave", args=(project.id,)) + response = client.post(url) + assert response.status_code == 403 + assert json.loads(response.content)["_error_message"] == "You can't leave the project if there are no more owners" + + +def test_leave_project_invalid_membership(client): + user = f.UserFactory.create() + project = f.ProjectFactory() + client.login(user) + url = reverse("projects-leave", args=(project.id,)) + response = client.post(url) + assert response.status_code == 404