diff --git a/taiga/export_import/api.py b/taiga/export_import/api.py index d7466276..1ae5ca4b 100644 --- a/taiga/export_import/api.py +++ b/taiga/export_import/api.py @@ -94,7 +94,7 @@ class ProjectImporterViewSet(mixins.ImportThrottlingPolicyMixin, CreateModelMixi is_private = data.get('is_private', False) (enough_slots, not_enough_slots_error) = users_service.has_available_slot_for_project( self.request.user, - project=Project(is_private=is_private, id=None) + Project(is_private=is_private, id=None) ) if not enough_slots: raise exc.NotEnoughSlotsForProject(is_private, 1, not_enough_slots_error) @@ -115,11 +115,11 @@ class ProjectImporterViewSet(mixins.ImportThrottlingPolicyMixin, CreateModelMixi # Create memberships if "memberships" in data: - members = len(data['memberships']) + members = len([m for m in data.get("memberships", []) if m.get("email", None) != data["owner"]]) (enough_slots, not_enough_slots_error) = users_service.has_available_slot_for_project( self.request.user, - project=Project(is_private=is_private, id=None), - members=max(members, 1) + Project(is_private=is_private, id=None), + members ) if not enough_slots: raise exc.NotEnoughSlotsForProject(is_private, max(members, 1), not_enough_slots_error) @@ -223,16 +223,18 @@ class ProjectImporterViewSet(mixins.ImportThrottlingPolicyMixin, CreateModelMixi except Exception: raise exc.WrongArguments(_("Invalid dump format")) - user = request.user slug = dump.get('slug', None) if slug is not None and Project.objects.filter(slug=slug).exists(): del dump['slug'] - members = len(dump.get("memberships", [])) + user = request.user + dump['owner'] = user.email + + members = len([m for m in dump.get("memberships", []) if m.get("email", None) != dump["owner"]]) (enough_slots, not_enough_slots_error) = users_service.has_available_slot_for_project( user, - project=Project(is_private=is_private, id=None), - members=max(members, 1) + Project(is_private=is_private, id=None), + members ) if not enough_slots: raise exc.NotEnoughSlotsForProject(is_private, max(members, 1), not_enough_slots_error) diff --git a/taiga/export_import/dump_service.py b/taiga/export_import/dump_service.py index b41d9534..b68f3bf9 100644 --- a/taiga/export_import/dump_service.py +++ b/taiga/export_import/dump_service.py @@ -91,11 +91,11 @@ def store_tags_colors(project, data): def dict_to_project(data, owner=None): if owner: data["owner"] = owner.email - members = len(data.get("memberships", [])) + members = len([m for m in data.get("memberships", []) if m.get("email", None) != data["owner"]]) (enough_slots, not_enough_slots_error) = users_service.has_available_slot_for_project( owner, - project=Project(is_private=data.get("is_private", False), id=None), - members=members + Project(is_private=data.get("is_private", False), id=None), + members ) if not enough_slots: raise TaigaImportError(not_enough_slots_error) diff --git a/taiga/projects/api.py b/taiga/projects/api.py index 345eed71..d32115c4 100644 --- a/taiga/projects/api.py +++ b/taiga/projects/api.py @@ -380,8 +380,7 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, (enough_slots, not_enough_slots_error) = users_service.has_available_slot_for_project( request.user, - project=project, - members=0 + project, ) if not enough_slots: members = project.memberships.count() @@ -419,16 +418,17 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, permissions_service.set_base_permissions_for_project(obj) def pre_save(self, obj): - user = self.request.user - (enough_slots, not_enough_slots_error) = users_service.has_available_slot_for_project(user, project=obj) - members = max(obj.memberships.count(), 1) - if not enough_slots: - raise exc.NotEnoughSlotsForProject(obj.is_private, members, not_enough_slots_error) - if not obj.id: - obj.owner = user + obj.owner = self.request.user obj.template = self.request.QUERY_PARAMS.get('template', None) + # Validate if the owner have enought slots to create or update the project + # TODO: Move to the ProjectAdminSerializer + (enough_slots, not_enough_slots_error) = users_service.has_available_slot_for_project(obj.owner, obj) + if not enough_slots: + members = max(obj.memberships.count(), 1) + raise exc.NotEnoughSlotsForProject(obj.is_private, members, not_enough_slots_error) + self._set_base_permissions(obj) super().pre_save(obj) @@ -635,8 +635,8 @@ class MembershipViewSet(BlockedByProjectMixin, ModelCrudViewSet): members = len(data["bulk_memberships"]) (enough_slots, not_enough_slots_error) = users_service.has_available_slot_for_project( project.owner, - project=project, - members=members + project, + members ) if not enough_slots: raise exc.NotEnoughSlotsForProject(project.is_private, members, not_enough_slots_error) @@ -672,8 +672,8 @@ class MembershipViewSet(BlockedByProjectMixin, ModelCrudViewSet): members = 1 (enough_slots, not_enough_slots_error) = users_service.has_available_slot_for_project( self.request.user, - project=obj.project, - members=members + obj.project, + members ) if not enough_slots: raise exc.NotEnoughSlotsForProject(obj.project.is_private, members, not_enough_slots_error) diff --git a/taiga/users/services.py b/taiga/users/services.py index d946bb58..b18e8b36 100644 --- a/taiga/users/services.py +++ b/taiga/users/services.py @@ -575,41 +575,60 @@ def get_voted_list(for_user, from_user, type=None, q=None): ] -def has_available_slot_for_project(user, project, members=1): +def has_available_slot_for_project(user, project, new_members=0): + # TODO: Refactor: Create one service for every type of action and move to project services + # + # - has_available_slot_to_create_new_project() + # - has_available_slot_to_update_this_project() + # - has_available_slot_to_transfer_this_project() + # - has_available_slot_to_import_this_project() + # - has_available_slot_to_add_members_to_this_project() + (enough, error) = _has_available_slot_for_project_type(user, project) if not enough: return (enough, error) - return _has_available_slot_for_project_members(user, project, members) + return _has_available_slot_for_project_members(user, project, new_members) def _has_available_slot_for_project_type(user, project): if project.is_private: if user.max_private_projects is None: return (True, None) - elif user.owned_projects.filter(is_private=True).exclude(id=project.id).count() < user.max_private_projects: + + current_private_projects = user.owned_projects.filter(is_private=True).exclude(id=project.id).count() + if current_private_projects < user.max_private_projects: return (True, None) + return (False, _("You can't have more private projects")) + else: if user.max_public_projects is None: return (True, None) - elif user.owned_projects.filter(is_private=False).exclude(id=project.id).count() < user.max_public_projects: + + current_public_project = user.owned_projects.filter(is_private=False).exclude(id=project.id).count() + if current_public_project < user.max_public_projects: return (True, None) + return (False, _("You can't have more public projects")) - -def _has_available_slot_for_project_members(user, project, members): - current_memberships = project.memberships.count() +def _has_available_slot_for_project_members(user, project, new_members): + current_memberships = max(project.memberships.count(), 1) if project.is_private: if user.max_memberships_private_projects is None: return (True, None) - elif current_memberships + members <= user.max_memberships_private_projects: + + if current_memberships + new_members <= user.max_memberships_private_projects: return (True, None) + return (False, _("You have reached your current limit of memberships for private projects")) + else: if user.max_memberships_public_projects is None: return (True, None) - elif current_memberships + members <= user.max_memberships_public_projects: + + if current_memberships + new_members <= user.max_memberships_public_projects: return (True, None) + return (False, _("You have reached your current limit of memberships for public projects")) diff --git a/tests/integration/test_importer_api.py b/tests/integration/test_importer_api.py index 023f5764..159abb76 100644 --- a/tests/integration/test_importer_api.py +++ b/tests/integration/test_importer_api.py @@ -1501,3 +1501,87 @@ def test_valid_dump_import_without_slug(client): response = client.post(url, {'dump': data}) assert response.status_code == 201 + + +def test_valid_dump_import_with_the_limit_of_membership_whit_you_for_private_project(client): + user = f.UserFactory.create(max_memberships_private_projects=5) + client.login(user) + + url = reverse("importer-load-dump") + + data = ContentFile(bytes(json.dumps({ + "slug": "private-project-with-memberships-limit-with-you", + "name": "Valid project", + "description": "Valid project desc", + "is_private": True, + "memberships": [ + { + "email": user.email, + "role": "Role", + }, + { + "email": "test2@test.com", + "role": "Role", + }, + { + "email": "test3@test.com", + "role": "Role", + }, + { + "email": "test4@test.com", + "role": "Role", + }, + { + "email": "test5@test.com", + "role": "Role", + }, + ], + "roles": [{"name": "Role"}] + }), "utf-8")) + data.name = "test" + + response = client.post(url, {'dump': data}) + assert response.status_code == 201 + assert Project.objects.filter(slug="private-project-with-memberships-limit-with-you").count() == 1 + + +def test_valid_dump_import_with_the_limit_of_membership_whit_you_for_public_project(client): + user = f.UserFactory.create(max_memberships_public_projects=5) + client.login(user) + + url = reverse("importer-load-dump") + + data = ContentFile(bytes(json.dumps({ + "slug": "public-project-with-memberships-limit-with-you", + "name": "Valid project", + "description": "Valid project desc", + "is_private": False, + "memberships": [ + { + "email": user.email, + "role": "Role", + }, + { + "email": "test2@test.com", + "role": "Role", + }, + { + "email": "test3@test.com", + "role": "Role", + }, + { + "email": "test4@test.com", + "role": "Role", + }, + { + "email": "test5@test.com", + "role": "Role", + }, + ], + "roles": [{"name": "Role"}] + }), "utf-8")) + data.name = "test" + + response = client.post(url, {'dump': data}) + assert response.status_code == 201 + assert Project.objects.filter(slug="public-project-with-memberships-limit-with-you").count() == 1