diff --git a/settings/common.py b/settings/common.py index fa1f9d73..7096f6e6 100644 --- a/settings/common.py +++ b/settings/common.py @@ -526,6 +526,8 @@ EXTRA_BLOCKING_CODES = [] MAX_PRIVATE_PROJECTS_PER_USER = None # None == no limit MAX_PUBLIC_PROJECTS_PER_USER = None # None == no limit +MAX_MEMBERS_PRIVATE_PROJECTS = None # None == no limit +MAX_MEMBERS_PUBLIC_PROJECTS = None # None == no limit from .sr import * diff --git a/taiga/export_import/api.py b/taiga/export_import/api.py index 6f20cc60..af472e3d 100644 --- a/taiga/export_import/api.py +++ b/taiga/export_import/api.py @@ -92,8 +92,12 @@ class ProjectImporterViewSet(mixins.ImportThrottlingPolicyMixin, CreateModelMixi data['owner'] = data.get('owner', request.user.email) is_private = data.get('is_private', False) - if not users_service.has_available_slot_for_project(self.request.user, is_private=is_private): - raise exc.BadRequest(_("The user can't have more projects of this type")) + (enough_slots, not_enough_slots_error) = users_service.has_available_slot_for_project( + self.request.user, + project=Project(is_private=is_private, id=None) + ) + if not enough_slots: + raise exc.BadRequest(not_enough_slots_error) # Create Project project_serialized = service.store_project(data) @@ -111,6 +115,14 @@ class ProjectImporterViewSet(mixins.ImportThrottlingPolicyMixin, CreateModelMixi # Create memberships if "memberships" in data: + members = len(data['memberships']) + (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) + ) + if not enough_slots: + raise exc.BadRequest(not_enough_slots_error) service.store_memberships(project_serialized.object, data) try: @@ -211,12 +223,25 @@ class ProjectImporterViewSet(mixins.ImportThrottlingPolicyMixin, CreateModelMixi except Exception: raise exc.WrongArguments(_("Invalid dump format")) + user = request.user + (enough_slots, not_enough_slots_error) = users_service.has_available_slot_for_project( + user, + project=Project(is_private=is_private, id=None) + ) + if not enough_slots: + raise exc.BadRequest(not_enough_slots_error) + if Project.objects.filter(slug=dump['slug']).exists(): del dump['slug'] - user = request.user - if not users_service.has_available_slot_for_project(user, is_private=is_private): - raise exc.BadRequest(_("The user can't have more projects of this type")) + members = len(dump.get("memberships", [])) + (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) + ) + if not enough_slots: + raise exc.BadRequest(not_enough_slots_error) if settings.CELERY_ENABLED: task = tasks.load_project_dump.delay(user, dump) diff --git a/taiga/export_import/dump_service.py b/taiga/export_import/dump_service.py index 0237145a..adbaa6fe 100644 --- a/taiga/export_import/dump_service.py +++ b/taiga/export_import/dump_service.py @@ -17,7 +17,7 @@ from django.utils.translation import ugettext as _ -from taiga.projects.models import Membership +from taiga.projects.models import Membership, Project from taiga.users import services as users_service from . import serializers @@ -91,8 +91,14 @@ def store_tags_colors(project, data): def dict_to_project(data, owner=None): if owner: data["owner"] = owner.email - if not users_service.has_available_slot_for_project(owner, is_private=data["is_private"]): - raise TaigaImportError(_("The user can't have more projects of this type")) + members = len(data.get("memberships", [])) + (enough_slots, not_enough_slots_error) = users_service.has_available_slot_for_project( + owner, + project=Project(is_private=data["is_private"], id=None), + members=members + ) + if not enough_slots: + raise TaigaImportError(not_enough_slots_error) project_serialized = service.store_project(data) diff --git a/taiga/export_import/management/commands/load_dump.py b/taiga/export_import/management/commands/load_dump.py index bea52417..367a2401 100644 --- a/taiga/export_import/management/commands/load_dump.py +++ b/taiga/export_import/management/commands/load_dump.py @@ -27,6 +27,7 @@ from taiga.export_import.dump_service import dict_to_project, TaigaImportError from taiga.export_import.service import get_errors from taiga.users.models import User + class Command(BaseCommand): args = ' ' help = 'Export a project to json' diff --git a/taiga/projects/api.py b/taiga/projects/api.py index 15017817..a5e2838c 100644 --- a/taiga/projects/api.py +++ b/taiga/projects/api.py @@ -344,8 +344,9 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, def pre_save(self, obj): user = self.request.user - if not users_service.has_available_slot_for_project(user, is_private=obj.is_private): - raise exc.BadRequest(_("The user can't have more projects of this type")) + (enough_slots, not_enough_slots_error) = users_service.has_available_slot_for_project(user, project=obj) + if not enough_slots: + raise exc.BadRequest(not_enough_slots_error) if not obj.id: obj.owner = user @@ -554,6 +555,15 @@ class MembershipViewSet(BlockedByProjectMixin, ModelCrudViewSet): # TODO: this should be moved to main exception handler instead # of handling explicit exception catchin here. + if "bulk_memberships" in data and isinstance(data["bulk_memberships"], list): + (enough_slots, not_enough_slots_error) = users_service.has_available_slot_for_project( + request.user, + project=project, + members=len(data["bulk_memberships"]) + ) + if not enough_slots: + raise exc.BadRequest(not_enough_slots_error) + try: members = services.create_members_in_bulk(data["bulk_memberships"], project=project, @@ -581,6 +591,15 @@ class MembershipViewSet(BlockedByProjectMixin, ModelCrudViewSet): raise exc.BadRequest(_("The project must have an owner and at least one of the users must be an active admin")) def pre_save(self, obj): + if not obj.id: + (enough_slots, not_enough_slots_error) = users_service.has_available_slot_for_project( + self.request.user, + project=obj.project, + members=1 + ) + if not enough_slots: + raise exc.BadRequest(not_enough_slots_error) + if not obj.token: obj.token = str(uuid.uuid1()) diff --git a/taiga/users/admin.py b/taiga/users/admin.py index d5a3a4a0..76bc701d 100644 --- a/taiga/users/admin.py +++ b/taiga/users/admin.py @@ -30,14 +30,14 @@ admin.site.unregister(Group) class RoleAdmin(admin.ModelAdmin): list_display = ["name"] - filter_horizontal = ('permissions',) + filter_horizontal = ("permissions",) def formfield_for_manytomany(self, db_field, request=None, **kwargs): - if db_field.name == 'permissions': - qs = kwargs.get('queryset', db_field.rel.to.objects) + if db_field.name == "permissions": + qs = kwargs.get("queryset", db_field.rel.to.objects) # Avoid a major performance hit resolving permission names which # triggers a content_type load: - kwargs['queryset'] = qs.select_related('content_type') + kwargs["queryset"] = qs.select_related("content_type") return super().formfield_for_manytomany( db_field, request=request, **kwargs) @@ -47,18 +47,21 @@ class RoleAdmin(admin.ModelAdmin): class UserAdmin(DjangoUserAdmin): fieldsets = ( - (None, {'fields': ('username', 'password')}), - (_('Personal info'), {'fields': ('full_name', 'email', 'bio', 'photo')}), - (_('Extra info'), {'fields': ('color', 'lang', 'timezone', 'token', 'colorize_tags', 'email_token', 'new_email')}), - (_('Permissions'), {'fields': ('is_active', 'is_superuser', 'max_private_projects', 'max_public_projects')}), - (_('Important dates'), {'fields': ('last_login', 'date_joined')}), + (None, {"fields": ("username", "password")}), + (_("Personal info"), {"fields": ("full_name", "email", "bio", "photo")}), + (_("Extra info"), {"fields": ("color", "lang", "timezone", "token", "colorize_tags", + "email_token", "new_email")}), + (_("Permissions"), {"fields": ("is_active", "is_superuser")}), + (_("Restrictions"), {"fields": (("max_private_projects", "max_members_private_projects"), + ("max_public_projects", "max_members_public_projects"))}), + (_("Important dates"), {"fields": ("last_login", "date_joined")}), ) form = UserChangeForm add_form = UserCreationForm - list_display = ('username', 'email', 'full_name') - list_filter = ('is_superuser', 'is_active') - search_fields = ('username', 'full_name', 'email') - ordering = ('username',) + list_display = ("username", "email", "full_name") + list_filter = ("is_superuser", "is_active") + search_fields = ("username", "full_name", "email") + ordering = ("username",) filter_horizontal = () class RoleInline(admin.TabularInline): diff --git a/taiga/users/migrations/0015_auto_20160120_1409.py b/taiga/users/migrations/0015_auto_20160120_1409.py index 3fea9616..2b62b760 100644 --- a/taiga/users/migrations/0015_auto_20160120_1409.py +++ b/taiga/users/migrations/0015_auto_20160120_1409.py @@ -2,6 +2,7 @@ from __future__ import unicode_literals from django.db import migrations, models +from django.conf import settings class Migration(migrations.Migration): @@ -14,11 +15,11 @@ class Migration(migrations.Migration): migrations.AddField( model_name='user', name='max_private_projects', - field=models.IntegerField(null=True, verbose_name='max number of private projects owned', default=None, blank=True), + field=models.IntegerField(null=True, verbose_name='max number of private projects owned', default=settings.MAX_PRIVATE_PROJECTS_PER_USER, blank=True), ), migrations.AddField( model_name='user', name='max_public_projects', - field=models.IntegerField(null=True, verbose_name='max number of public projects owned', default=None, blank=True), + field=models.IntegerField(null=True, verbose_name='max number of public projects owned', default=settings.MAX_PUBLIC_PROJECTS_PER_USER, blank=True), ), ] diff --git a/taiga/users/migrations/0016_auto_20160204_1050.py b/taiga/users/migrations/0016_auto_20160204_1050.py new file mode 100644 index 00000000..d148f56a --- /dev/null +++ b/taiga/users/migrations/0016_auto_20160204_1050.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models +from django.conf import settings + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0015_auto_20160120_1409'), + ] + + operations = [ + migrations.AddField( + model_name='user', + name='max_members_private_projects', + field=models.IntegerField(default=settings.MAX_MEMBERS_PRIVATE_PROJECTS, blank=True, verbose_name='max number of memberships for each owned private project', null=True), + ), + migrations.AddField( + model_name='user', + name='max_members_public_projects', + field=models.IntegerField(default=settings.MAX_MEMBERS_PUBLIC_PROJECTS, blank=True, verbose_name='max number of memberships for each owned public project', null=True), + ), + ] diff --git a/taiga/users/models.py b/taiga/users/models.py index 9ce48998..e55bf72c 100644 --- a/taiga/users/models.py +++ b/taiga/users/models.py @@ -72,11 +72,11 @@ def get_user_file_path(instance, filename): class PermissionsMixin(models.Model): """ A mixin class that adds the fields and methods necessary to support - Django's Permission model using the ModelBackend. + Django"s Permission model using the ModelBackend. """ - is_superuser = models.BooleanField(_('superuser status'), default=False, - help_text=_('Designates that this user has all permissions without ' - 'explicitly assigning them.')) + is_superuser = models.BooleanField(_("superuser status"), default=False, + help_text=_("Designates that this user has all permissions without " + "explicitly assigning them.")) class Meta: abstract = True @@ -105,25 +105,25 @@ class PermissionsMixin(models.Model): class User(AbstractBaseUser, PermissionsMixin): - username = models.CharField(_('username'), max_length=255, unique=True, - help_text=_('Required. 30 characters or fewer. Letters, numbers and ' - '/./-/_ characters'), + username = models.CharField(_("username"), max_length=255, unique=True, + help_text=_("Required. 30 characters or fewer. Letters, numbers and " + "/./-/_ characters"), validators=[ - validators.RegexValidator(re.compile('^[\w.-]+$'), _('Enter a valid username.'), 'invalid') + validators.RegexValidator(re.compile("^[\w.-]+$"), _("Enter a valid username."), "invalid") ]) - email = models.EmailField(_('email address'), max_length=255, blank=True, unique=True) - is_active = models.BooleanField(_('active'), default=True, - help_text=_('Designates whether this user should be treated as ' - 'active. Unselect this instead of deleting accounts.')) + email = models.EmailField(_("email address"), max_length=255, blank=True, unique=True) + is_active = models.BooleanField(_("active"), default=True, + help_text=_("Designates whether this user should be treated as " + "active. Unselect this instead of deleting accounts.")) - full_name = models.CharField(_('full name'), max_length=256, blank=True) + full_name = models.CharField(_("full name"), max_length=256, blank=True) color = models.CharField(max_length=9, null=False, blank=True, default=generate_random_hex_color, verbose_name=_("color")) bio = models.TextField(null=False, blank=True, default="", verbose_name=_("biography")) photo = models.FileField(upload_to=get_user_file_path, max_length=500, null=True, blank=True, verbose_name=_("photo")) - date_joined = models.DateTimeField(_('date joined'), default=timezone.now) + date_joined = models.DateTimeField(_("date joined"), default=timezone.now) lang = models.CharField(max_length=20, null=True, blank=True, default="", verbose_name=_("default language")) theme = models.CharField(max_length=100, null=True, blank=True, default="", @@ -138,21 +138,33 @@ class User(AbstractBaseUser, PermissionsMixin): email_token = models.CharField(max_length=200, null=True, blank=True, default=None, verbose_name=_("email token")) - new_email = models.EmailField(_('new email address'), null=True, blank=True) + new_email = models.EmailField(_("new email address"), null=True, blank=True) is_system = models.BooleanField(null=False, blank=False, default=False) - max_private_projects = models.IntegerField(null=True, blank=True, default=settings.MAX_PRIVATE_PROJECTS_PER_USER, verbose_name='max number of private projects owned') - max_public_projects = models.IntegerField(null=True, blank=True, default=settings.MAX_PUBLIC_PROJECTS_PER_USER, verbose_name='max number of public projects owned') + max_private_projects = models.IntegerField(null=True, blank=True, + default=settings.MAX_PRIVATE_PROJECTS_PER_USER, + verbose_name=_("max number of private projects owned")) + max_public_projects = models.IntegerField(null=True, blank=True, + default=settings.MAX_PUBLIC_PROJECTS_PER_USER, + verbose_name=_("max number of public projects owned")) + max_members_private_projects = models.IntegerField(null=True, blank=True, + default=settings.MAX_MEMBERS_PRIVATE_PROJECTS, + verbose_name=_("max number of memberships for " + "each owned private project")) + max_members_public_projects = models.IntegerField(null=True, blank=True, + default=settings.MAX_MEMBERS_PUBLIC_PROJECTS, + verbose_name=_("max number of memberships for " + "each owned public project")) _cached_memberships = None _cached_liked_ids = None _cached_watched_ids = None _cached_notify_levels = None - USERNAME_FIELD = 'username' - REQUIRED_FIELDS = ['email'] + USERNAME_FIELD = "username" + REQUIRED_FIELDS = ["email"] objects = UserManager() @@ -160,9 +172,6 @@ class User(AbstractBaseUser, PermissionsMixin): verbose_name = "user" verbose_name_plural = "users" ordering = ["username"] - permissions = ( - ("view_user", "Can view user"), - ) def __str__(self): return self.get_full_name() @@ -285,16 +294,13 @@ class Role(models.Model): verbose_name_plural = "roles" ordering = ["order", "slug"] unique_together = (("slug", "project"),) - permissions = ( - ("view_role", "Can view role"), - ) def __str__(self): return self.name class AuthData(models.Model): - user = models.ForeignKey('users.User', related_name="auth_data") + user = models.ForeignKey("users.User", related_name="auth_data") key = models.SlugField(max_length=50) value = models.CharField(max_length=300) extra = JsonField() diff --git a/taiga/users/serializers.py b/taiga/users/serializers.py index bfceda55..204852ad 100644 --- a/taiga/users/serializers.py +++ b/taiga/users/serializers.py @@ -115,10 +115,13 @@ class UserAdminSerializer(UserSerializer): "color", "bio", "lang", "theme", "timezone", "is_active", "photo", "big_photo", "max_private_projects", "max_public_projects", + "max_members_private_projects", "max_members_public_projects", "total_private_projects", "total_public_projects") read_only_fields = ("id", "email", - "max_private_projects", "max_public_projects") + "max_private_projects", "max_public_projects", + "max_members_private_projects", + "max_members_public_projects") def get_total_private_projects(self, user): return user.owned_projects.filter(is_private=True).count() diff --git a/taiga/users/services.py b/taiga/users/services.py index 45dd4be9..54ce79d7 100644 --- a/taiga/users/services.py +++ b/taiga/users/services.py @@ -574,14 +574,41 @@ def get_voted_list(for_user, from_user, type=None, q=None): ] -def has_available_slot_for_project(user, is_private=False): - if is_private: +def has_available_slot_for_project(user, project, members=1): + (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) + + +def _has_available_slot_for_project_type(user, project): + if project.is_private: if user.max_private_projects is None: - return True + return (True, None) + elif user.owned_projects.filter(is_private=True).exclude(id=project.id).count() < 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: + return (True, None) + return (False, _("You can't have more public projects")) - return user.owned_projects.filter(is_private=True).count() < user.max_private_projects - if user.max_public_projects is None: - return True - return user.owned_projects.filter(is_private=False).count() < user.max_public_projects +def _has_available_slot_for_project_members(user, project, members): + current_memberships = project.memberships.count() + + if project.is_private: + if user.max_members_private_projects is None: + return (True, None) + elif current_memberships + members <= user.max_members_private_projects: + return (True, None) + return (False, _("You have reached the limit of memberships for private projects")) + else: + if user.max_members_public_projects is None: + return (True, None) + elif current_memberships + members <= user.max_members_public_projects: + return (True, None) + return (False, _("You have reached the limit of memberships for public projects")) diff --git a/tests/integration/test_importer_api.py b/tests/integration/test_importer_api.py index 996e45c6..52784d47 100644 --- a/tests/integration/test_importer_api.py +++ b/tests/integration/test_importer_api.py @@ -89,7 +89,7 @@ def test_valid_project_without_enough_public_projects_slots(client): response = client.json.post(url, json.dumps(data)) assert response.status_code == 400 - assert "can't have more projects" in response.data["_error_message"] + assert "can't have more public projects" in response.data["_error_message"] assert Project.objects.filter(slug="public-project-without-slots").count() == 0 @@ -109,7 +109,7 @@ def test_valid_project_without_enough_private_projects_slots(client): response = client.json.post(url, json.dumps(data)) assert response.status_code == 400 - assert "can't have more projects" in response.data["_error_message"] + assert "can't have more private projects" in response.data["_error_message"] assert Project.objects.filter(slug="private-project-without-slots").count() == 0 @@ -1010,7 +1010,7 @@ def test_milestone_import_duplicated_milestone(client): assert response_data["milestones"][0]["name"][0] == "Name duplicated for the project" -def test_dict_to_project_with_no_slots_available(client): +def test_dict_to_project_with_no_projects_slots_available(client): user = f.UserFactory.create(max_private_projects=0) data = { @@ -1023,7 +1023,77 @@ def test_dict_to_project_with_no_slots_available(client): with pytest.raises(TaigaImportError) as excinfo: project = dict_to_project(data, owner=user) - assert "can't have more projects" in str(excinfo.value) + assert "can't have more private projects" in str(excinfo.value) + + +def test_dict_to_project_with_no_members_private_project_slots_available(client): + user = f.UserFactory.create(max_members_private_projects=2) + + data = { + "slug": "valid-project", + "name": "Valid project", + "description": "Valid project desc", + "is_private": True, + "roles": [{"name": "Role"}], + "memberships": [ + { + "email": "test1@test.com", + "role": "Role", + }, + { + "email": "test2@test.com", + "role": "Role", + }, + { + "email": "test3@test.com", + "role": "Role", + }, + { + "email": "test4@test.com", + "role": "Role", + } + ] + } + + with pytest.raises(TaigaImportError) as excinfo: + project = dict_to_project(data, owner=user) + + assert "reached the limit of memberships for private" in str(excinfo.value) + + +def test_dict_to_project_with_no_members_public_project_slots_available(client): + user = f.UserFactory.create(max_members_public_projects=2) + + data = { + "slug": "valid-project", + "name": "Valid project", + "description": "Valid project desc", + "is_private": False, + "roles": [{"name": "Role"}], + "memberships": [ + { + "email": "test1@test.com", + "role": "Role", + }, + { + "email": "test2@test.com", + "role": "Role", + }, + { + "email": "test3@test.com", + "role": "Role", + }, + { + "email": "test4@test.com", + "role": "Role", + } + ] + } + + with pytest.raises(TaigaImportError) as excinfo: + project = dict_to_project(data, owner=user) + + assert "reached the limit of memberships for public" in str(excinfo.value) def test_invalid_dump_import(client): @@ -1053,6 +1123,7 @@ def test_valid_dump_import_with_logo(client, settings): "slug": "valid-project", "name": "Valid project", "description": "Valid project desc", + "is_private": False, "logo": { "name": "logo.bmp", "data": base64.b64encode(DUMMY_BMP_DATA).decode("utf-8") @@ -1177,7 +1248,7 @@ def test_valid_dump_import_without_enough_public_projects_slots(client): response = client.post(url, {'dump': data}) assert response.status_code == 400 - assert "can't have more projects" in response.data["_error_message"] + assert "can't have more public projects" in response.data["_error_message"] assert Project.objects.filter(slug="public-project-without-slots").count() == 0 @@ -1197,5 +1268,211 @@ def test_valid_dump_import_without_enough_private_projects_slots(client): response = client.post(url, {'dump': data}) assert response.status_code == 400 - assert "can't have more projects" in response.data["_error_message"] + assert "can't have more private projects" in response.data["_error_message"] assert Project.objects.filter(slug="private-project-without-slots").count() == 0 + + +def test_valid_dump_import_without_enough_membership_private_project_slots_one_project(client): + user = f.UserFactory.create(max_members_private_projects=5) + client.login(user) + + url = reverse("importer-load-dump") + + data = ContentFile(bytes(json.dumps({ + "slug": "project-without-memberships-slots", + "name": "Valid project", + "description": "Valid project desc", + "is_private": True, + "memberships": [ + { + "email": "test1@test.com", + "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", + }, + { + "email": "test6@test.com", + "role": "Role", + }, + ], + "roles": [{"name": "Role"}] + }), "utf-8")) + data.name = "test" + + response = client.post(url, {'dump': data}) + assert response.status_code == 400 + assert "reached the limit of memberships for private" in response.data["_error_message"] + assert Project.objects.filter(slug="project-without-memberships-slots").count() == 0 + + +def test_valid_dump_import_without_enough_membership_public_project_slots_one_project(client): + user = f.UserFactory.create(max_members_public_projects=5) + client.login(user) + + url = reverse("importer-load-dump") + + data = ContentFile(bytes(json.dumps({ + "slug": "project-without-memberships-slots", + "name": "Valid project", + "description": "Valid project desc", + "is_private": False, + "memberships": [ + { + "email": "test1@test.com", + "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", + }, + { + "email": "test6@test.com", + "role": "Role", + }, + ], + "roles": [{"name": "Role"}] + }), "utf-8")) + data.name = "test" + + response = client.post(url, {'dump': data}) + assert response.status_code == 400 + assert "reached the limit of memberships for public" in response.data["_error_message"] + assert Project.objects.filter(slug="project-without-memberships-slots").count() == 0 + + +def test_valid_dump_import_with_enough_membership_private_project_slots_multiple_projects(client, settings): + settings.CELERY_ENABLED = False + + user = f.UserFactory.create(max_members_private_projects=10) + project = f.ProjectFactory.create(owner=user) + f.MembershipFactory.create(project=project) + f.MembershipFactory.create(project=project) + f.MembershipFactory.create(project=project) + f.MembershipFactory.create(project=project) + f.MembershipFactory.create(project=project) + client.login(user) + + url = reverse("importer-load-dump") + + data = ContentFile(bytes(json.dumps({ + "slug": "project-without-memberships-slots", + "name": "Valid project", + "description": "Valid project desc", + "is_private": True, + "roles": [{"name": "Role"}], + "memberships": [ + { + "email": "test1@test.com", + "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", + }, + { + "email": "test6@test.com", + "role": "Role", + } + ] + }), "utf-8")) + data.name = "test" + + response = client.post(url, {'dump': data}) + assert response.status_code == 201 + response_data = response.data + assert "id" in response_data + assert response_data["name"] == "Valid project" + + +def test_valid_dump_import_with_enough_membership_public_project_slots_multiple_projects(client, settings): + settings.CELERY_ENABLED = False + + user = f.UserFactory.create(max_members_public_projects=10) + project = f.ProjectFactory.create(owner=user) + f.MembershipFactory.create(project=project) + f.MembershipFactory.create(project=project) + f.MembershipFactory.create(project=project) + f.MembershipFactory.create(project=project) + f.MembershipFactory.create(project=project) + client.login(user) + + url = reverse("importer-load-dump") + + data = ContentFile(bytes(json.dumps({ + "slug": "project-without-memberships-slots", + "name": "Valid project", + "description": "Valid project desc", + "is_private": False, + "roles": [{"name": "Role"}], + "memberships": [ + { + "email": "test1@test.com", + "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", + }, + { + "email": "test6@test.com", + "role": "Role", + } + ] + }), "utf-8")) + data.name = "test" + + response = client.post(url, {'dump': data}) + assert response.status_code == 201 + response_data = response.data + assert "id" in response_data + assert response_data["name"] == "Valid project" diff --git a/tests/integration/test_memberships.py b/tests/integration/test_memberships.py index 75d7f29a..ec4e3124 100644 --- a/tests/integration/test_memberships.py +++ b/tests/integration/test_memberships.py @@ -53,6 +53,112 @@ def test_api_create_bulk_members(client): assert response.data[1]["email"] == joseph.email +def test_api_create_bulk_members_without_enough_memberships_private_project_slots_one_project(client): + user = f.UserFactory.create(max_members_private_projects=3) + project = f.ProjectFactory(owner=user, is_private=True) + role = f.RoleFactory(project=project, name="Test") + f.MembershipFactory(project=project, user=user, is_owner=True) + + url = reverse("memberships-bulk-create") + + data = { + "project_id": project.id, + "bulk_memberships": [ + {"role_id": role.pk, "email": "test1@test.com"}, + {"role_id": role.pk, "email": "test2@test.com"}, + {"role_id": role.pk, "email": "test3@test.com"}, + {"role_id": role.pk, "email": "test4@test.com"}, + ] + } + client.login(user) + response = client.json.post(url, json.dumps(data)) + + assert response.status_code == 400 + assert "reached the limit of memberships for private" in response.data["_error_message"] + + +def test_api_create_bulk_members_with_enough_memberships_private_project_slots_multiple_projects(client): + user = f.UserFactory.create(max_members_private_projects=6) + project = f.ProjectFactory(owner=user, is_private=True) + role = f.RoleFactory(project=project, name="Test") + f.MembershipFactory(project=project, user=user, is_owner=True) + + other_project = f.ProjectFactory(owner=user) + f.MembershipFactory.create(project=other_project) + f.MembershipFactory.create(project=other_project) + f.MembershipFactory.create(project=other_project) + f.MembershipFactory.create(project=other_project) + + url = reverse("memberships-bulk-create") + + data = { + "project_id": project.id, + "bulk_memberships": [ + {"role_id": role.pk, "email": "test1@test.com"}, + {"role_id": role.pk, "email": "test2@test.com"}, + {"role_id": role.pk, "email": "test3@test.com"}, + {"role_id": role.pk, "email": "test4@test.com"}, + ] + } + client.login(user) + response = client.json.post(url, json.dumps(data)) + + assert response.status_code == 200 + + +def test_api_create_bulk_members_without_enough_memberships_public_project_slots_one_project(client): + user = f.UserFactory.create(max_members_public_projects=3) + project = f.ProjectFactory(owner=user, is_private=False) + role = f.RoleFactory(project=project, name="Test") + f.MembershipFactory(project=project, user=user, is_owner=True) + + url = reverse("memberships-bulk-create") + + data = { + "project_id": project.id, + "bulk_memberships": [ + {"role_id": role.pk, "email": "test1@test.com"}, + {"role_id": role.pk, "email": "test2@test.com"}, + {"role_id": role.pk, "email": "test3@test.com"}, + {"role_id": role.pk, "email": "test4@test.com"}, + ] + } + client.login(user) + response = client.json.post(url, json.dumps(data)) + + assert response.status_code == 400 + assert "reached the limit of memberships for public" in response.data["_error_message"] + + +def test_api_create_bulk_members_with_enough_memberships_public_project_slots_multiple_projects(client): + user = f.UserFactory.create(max_members_public_projects=6) + project = f.ProjectFactory(owner=user, is_private=False) + role = f.RoleFactory(project=project, name="Test") + f.MembershipFactory(project=project, user=user, is_owner=True) + + other_project = f.ProjectFactory(owner=user) + f.MembershipFactory.create(project=other_project) + f.MembershipFactory.create(project=other_project) + f.MembershipFactory.create(project=other_project) + f.MembershipFactory.create(project=other_project) + + url = reverse("memberships-bulk-create") + + data = { + "project_id": project.id, + "bulk_memberships": [ + {"role_id": role.pk, "email": "test1@test.com"}, + {"role_id": role.pk, "email": "test2@test.com"}, + {"role_id": role.pk, "email": "test3@test.com"}, + {"role_id": role.pk, "email": "test4@test.com"}, + ] + } + client.login(user) + response = client.json.post(url, json.dumps(data)) + + assert response.status_code == 200 + + def test_api_create_bulk_members_with_extra_text(client, outbox): project = f.ProjectFactory() tester = f.RoleFactory(project=project, name="Tester") @@ -162,6 +268,76 @@ def test_api_create_membership(client): assert response.data["user_email"] == user.email +def test_api_create_membership_without_enough_memberships_private_project_slots_one_projects(client): + user = f.UserFactory.create(max_members_private_projects=1) + project = f.ProjectFactory(owner=user, is_private=True) + role = f.RoleFactory(project=project, name="Test") + f.MembershipFactory(project=project, user=user, is_owner=True) + + client.login(user) + url = reverse("memberships-list") + data = {"role": role.pk, "project": project.pk, "email": "test@test.com"} + response = client.json.post(url, json.dumps(data)) + + assert response.status_code == 400 + assert "reached the limit of memberships for private" in response.data["_error_message"] + + +def test_api_create_membership_with_enough_memberships_private_project_slots_multiple_projects(client): + user = f.UserFactory.create(max_members_private_projects=5) + project = f.ProjectFactory(owner=user, is_private=True) + role = f.RoleFactory(project=project, name="Test") + f.MembershipFactory(project=project, user=user, is_owner=True) + + other_project = f.ProjectFactory(owner=user) + f.MembershipFactory.create(project=other_project) + f.MembershipFactory.create(project=other_project) + f.MembershipFactory.create(project=other_project) + f.MembershipFactory.create(project=other_project) + + client.login(user) + url = reverse("memberships-list") + data = {"role": role.pk, "project": project.pk, "email": "test@test.com"} + response = client.json.post(url, json.dumps(data)) + + assert response.status_code == 201 + + +def test_api_create_membership_without_enough_memberships_public_project_slots_one_projects(client): + user = f.UserFactory.create(max_members_public_projects=1) + project = f.ProjectFactory(owner=user, is_private=False) + role = f.RoleFactory(project=project, name="Test") + f.MembershipFactory(project=project, user=user, is_owner=True) + + client.login(user) + url = reverse("memberships-list") + data = {"role": role.pk, "project": project.pk, "email": "test@test.com"} + response = client.json.post(url, json.dumps(data)) + + assert response.status_code == 400 + assert "reached the limit of memberships for public" in response.data["_error_message"] + + +def test_api_create_membership_with_enough_memberships_public_project_slots_multiple_projects(client): + user = f.UserFactory.create(max_members_public_projects=5) + project = f.ProjectFactory(owner=user, is_private=False) + role = f.RoleFactory(project=project, name="Test") + f.MembershipFactory(project=project, user=user, is_owner=True) + + other_project = f.ProjectFactory(owner=user) + f.MembershipFactory.create(project=other_project) + f.MembershipFactory.create(project=other_project) + f.MembershipFactory.create(project=other_project) + f.MembershipFactory.create(project=other_project) + + client.login(user) + url = reverse("memberships-list") + data = {"role": role.pk, "project": project.pk, "email": "test@test.com"} + response = client.json.post(url, json.dumps(data)) + + assert response.status_code == 201 + + def test_api_edit_membership(client): membership = f.MembershipFactory(is_owner=True) client.login(membership.user) diff --git a/tests/integration/test_projects.py b/tests/integration/test_projects.py index 603b327a..1627d537 100644 --- a/tests/integration/test_projects.py +++ b/tests/integration/test_projects.py @@ -56,7 +56,7 @@ def test_create_private_project_without_enough_private_projects_slots(client): response = client.json.post(url, json.dumps(data)) assert response.status_code == 400 - assert "can't have more projects" in response.data["_error_message"] + assert "can't have more private projects" in response.data["_error_message"] def test_create_public_project_without_enough_public_projects_slots(client): @@ -72,7 +72,7 @@ def test_create_public_project_without_enough_public_projects_slots(client): response = client.json.post(url, json.dumps(data)) assert response.status_code == 400 - assert "can't have more projects" in response.data["_error_message"] + assert "can't have more public projects" in response.data["_error_message"] def test_change_project_from_private_to_public_without_enough_public_projects_slots(client): @@ -88,7 +88,7 @@ def test_change_project_from_private_to_public_without_enough_public_projects_sl response = client.json.patch(url, json.dumps(data)) assert response.status_code == 400 - assert "can't have more projects" in response.data["_error_message"] + assert "can't have more public projects" in response.data["_error_message"] def test_change_project_from_public_to_private_without_enough_private_projects_slots(client): @@ -104,7 +104,7 @@ def test_change_project_from_public_to_private_without_enough_private_projects_s response = client.json.patch(url, json.dumps(data)) assert response.status_code == 400 - assert "can't have more projects" in response.data["_error_message"] + assert "can't have more private projects" in response.data["_error_message"] def test_create_private_project_with_enough_private_projects_slots(client): @@ -167,6 +167,21 @@ def test_change_project_from_public_to_private_with_enough_private_projects_slot assert response.status_code == 200 +def test_change_project_other_data_with_enough_private_projects_slots(client): + project = f.create_project(is_private=True, owner__max_private_projects=1) + f.MembershipFactory(user=project.owner, project=project, is_owner=True) + url = reverse("projects-detail", kwargs={"pk": project.pk}) + + data = { + "name": "test-project-change" + } + + client.login(project.owner) + response = client.json.patch(url, json.dumps(data)) + + assert response.status_code == 200 + + def test_partially_update_project(client): project = f.create_project() f.MembershipFactory(user=project.owner, project=project, is_owner=True)