Merge pull request #859 from taigaio/improving-contacts-endpoint

Improving contacts endpoints from users API
remotes/origin/issue/4795/notification_even_they_are_disabled
David Barragán Merino 2016-11-10 20:39:25 +01:00 committed by GitHub
commit b15b346f4c
11 changed files with 365 additions and 139 deletions

View File

@ -618,13 +618,24 @@ class ChoiceField(WritableField):
return value return value
class InvalidEmailValidationError(ValidationError):
pass
class InvalidDomainValidationError(ValidationError):
pass
def validate_user_email_allowed_domains(value): def validate_user_email_allowed_domains(value):
validators.validate_email(value) try:
validators.validate_email(value)
except ValidationError as e:
raise InvalidEmailValidationError(e)
domain_name = value.split("@")[1] domain_name = value.split("@")[1]
if settings.USER_EMAIL_ALLOWED_DOMAINS and domain_name not in settings.USER_EMAIL_ALLOWED_DOMAINS: if settings.USER_EMAIL_ALLOWED_DOMAINS and domain_name not in settings.USER_EMAIL_ALLOWED_DOMAINS:
raise ValidationError(_("You email domain is not allowed")) raise InvalidDomainValidationError(_("You email domain is not allowed"))
class EmailField(CharField): class EmailField(CharField):

View File

@ -653,7 +653,6 @@ class ProjectTemplateViewSet(ModelCrudViewSet):
class MembershipViewSet(BlockedByProjectMixin, ModelCrudViewSet): class MembershipViewSet(BlockedByProjectMixin, ModelCrudViewSet):
model = models.Membership model = models.Membership
admin_serializer_class = serializers.MembershipAdminSerializer admin_serializer_class = serializers.MembershipAdminSerializer
admin_validator_class = validators.MembershipAdminValidator
serializer_class = serializers.MembershipSerializer serializer_class = serializers.MembershipSerializer
validator_class = validators.MembershipValidator validator_class = validators.MembershipValidator
permission_classes = (permissions.MembershipPermission,) permission_classes = (permissions.MembershipPermission,)
@ -680,12 +679,6 @@ class MembershipViewSet(BlockedByProjectMixin, ModelCrudViewSet):
else: else:
return self.serializer_class return self.serializer_class
def get_validator_class(self):
if self.action == "create":
return self.admin_validator_class
return self.validator_class
def _check_if_project_can_have_more_memberships(self, project, total_new_memberships): def _check_if_project_can_have_more_memberships(self, project, total_new_memberships):
(can_add_memberships, error_type) = services.check_if_project_can_have_more_memberships( (can_add_memberships, error_type) = services.check_if_project_can_have_more_memberships(
project, project,
@ -700,7 +693,10 @@ class MembershipViewSet(BlockedByProjectMixin, ModelCrudViewSet):
@list_route(methods=["POST"]) @list_route(methods=["POST"])
def bulk_create(self, request, **kwargs): def bulk_create(self, request, **kwargs):
validator = validators.MembersBulkValidator(data=request.DATA) context = {
"request": request
}
validator = validators.MembersBulkValidator(data=request.DATA, context=context)
if not validator.is_valid(): if not validator.is_valid():
return response.BadRequest(validator.errors) return response.BadRequest(validator.errors)

View File

@ -16,11 +16,16 @@
# You should have received a copy of the GNU Affero General Public License # You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from taiga.base.exceptions import ValidationError
from taiga.base.utils import db, text from taiga.base.utils import db, text
from taiga.users.models import User
from django.core.validators import validate_email
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from .. import models from .. import models
def get_members_from_bulk(bulk_data, **additional_fields): def get_members_from_bulk(bulk_data, **additional_fields):
"""Convert `bulk_data` into a list of members. """Convert `bulk_data` into a list of members.
@ -32,6 +37,15 @@ def get_members_from_bulk(bulk_data, **additional_fields):
members = [] members = []
for data in bulk_data: for data in bulk_data:
data_copy = data.copy() data_copy = data.copy()
username = data_copy.pop("username")
try:
validate_email(username)
data_copy["email"] = username
except ValidationError:
user = User.objects.filter(username=username).first()
data_copy["user_id"] = user.id
data_copy.update(additional_fields) data_copy.update(additional_fields)
members.append(models.Membership(**data_copy)) members.append(models.Membership(**data_copy))
return members return members
@ -40,7 +54,7 @@ def get_members_from_bulk(bulk_data, **additional_fields):
def create_members_in_bulk(bulk_data, callback=None, precall=None, **additional_fields): def create_members_in_bulk(bulk_data, callback=None, precall=None, **additional_fields):
"""Create members from `bulk_data`. """Create members from `bulk_data`.
:param bulk_data: List of dicts `{"project_id": <>, "role_id": <>, "email": <>}`. :param bulk_data: List of dicts `{"project_id": <>, "role_id": <>, "username": <>}`.
:param callback: Callback to execute after each task save. :param callback: Callback to execute after each task save.
:param additional_fields: Additional fields when instantiating each task. :param additional_fields: Additional fields when instantiating each task.
@ -116,7 +130,7 @@ def check_if_project_can_have_more_memberships(project, total_new_memberships):
""" """
if project.owner is None: if project.owner is None:
return False, _("Project without owner") return False, _("Project without owner")
if project.is_private: if project.is_private:
total_memberships = project.memberships.count() + total_new_memberships total_memberships = project.memberships.count() + total_new_memberships
max_memberships = project.owner.max_memberships_private_projects max_memberships = project.owner.max_memberships_private_projects

View File

@ -16,15 +16,19 @@
# You should have received a copy of the GNU Affero General Public License # You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from django.core.validators import validate_email
from django.db.models import Q from django.db.models import Q
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from taiga.base.api import serializers from taiga.base.api import serializers
from taiga.base.api import validators from taiga.base.api import validators
from taiga.base.api.fields import validate_user_email_allowed_domains, InvalidEmailValidationError
from taiga.base.exceptions import ValidationError from taiga.base.exceptions import ValidationError
from taiga.base.fields import JSONField from taiga.base.fields import JSONField
from taiga.base.fields import PgArrayField from taiga.base.fields import PgArrayField
from taiga.users.models import Role from taiga.users.models import User, Role
from taiga.users import filters as user_filters
from .tagging.fields import TagsField from .tagging.fields import TagsField
@ -112,21 +116,24 @@ class IssueTypeValidator(DuplicatedNameInProjectValidator, validators.ModelValid
###################################################### ######################################################
class MembershipValidator(validators.ModelValidator): class MembershipValidator(validators.ModelValidator):
email = serializers.EmailField(required=True) username = serializers.CharField(required=True)
# email = serializers.EmailField(required=False)
# user = serializers.PrimaryKeyRelatedField(required=False)
class Meta: class Meta:
model = models.Membership model = models.Membership
# IMPORTANT: Maintain the MembershipAdminSerializer Meta up to date read_only_fields = ("user", "email")
# with this info (excluding here user_email and email)
read_only_fields = ("user",)
exclude = ("token", "email")
def validate_email(self, attrs, source): def restore_object(self, attrs, instance=None):
project = attrs.get("project", None) username = attrs.pop("username", None)
obj = super(MembershipValidator, self).restore_object(attrs, instance=instance)
obj.username = username
return obj
def _validate_member_doesnt_exist(self, attrs, email):
project = attrs.get("project", None if self.object is None else self.object.project)
if project is None: if project is None:
project = self.object.project return attrs
email = attrs[source]
qs = models.Membership.objects.all() qs = models.Membership.objects.all()
@ -139,14 +146,12 @@ class MembershipValidator(validators.ModelValidator):
Q(project_id=project.id, email=email)) Q(project_id=project.id, email=email))
if qs.count() > 0: if qs.count() > 0:
raise ValidationError(_("Email address is already taken")) raise ValidationError(_("The user yet exists in the project"))
return attrs
def validate_role(self, attrs, source): def validate_role(self, attrs, source):
project = attrs.get("project", None) project = attrs.get("project", None if self.object is None else self.object.project)
if project is None: if project is None:
project = self.object.project return attrs
role = attrs[source] role = attrs[source]
@ -155,10 +160,35 @@ class MembershipValidator(validators.ModelValidator):
return attrs return attrs
def validate_username(self, attrs, source):
username = attrs.get(source, None)
try:
validate_user_email_allowed_domains(username)
except ValidationError:
# If the validation comes from a request let's check the user is a valid contact
request = self.context.get("request", None)
if request is not None and request.user.is_authenticated():
valid_usernames = request.user.contacts_visible_by_user(request.user).values_list("username", flat=True)
if username not in valid_usernames:
raise ValidationError(_("The user must be a valid contact"))
user = User.objects.filter(Q(username=username) | Q(email=username)).first()
if user is not None:
email = user.email
self.user = user
else:
email = username
self.email = email
self._validate_member_doesnt_exist(attrs, email)
return attrs
def validate_is_admin(self, attrs, source): def validate_is_admin(self, attrs, source):
project = attrs.get("project", None) project = attrs.get("project", None if self.object is None else self.object.project)
if project is None: if project is None:
project = self.object.project return attrs
if (self.object and self.object.user): if (self.object and self.object.user):
if self.object.user.id == project.owner_id and not attrs[source]: if self.object.user.id == project.owner_id and not attrs[source]:
@ -171,20 +201,34 @@ class MembershipValidator(validators.ModelValidator):
return attrs return attrs
def is_valid(self):
errors = super().is_valid()
if hasattr(self, "email") and self.object is not None:
self.object.email = self.email
class MembershipAdminValidator(MembershipValidator): if hasattr(self, "user") and self.object is not None:
class Meta: self.object.user = self.user
model = models.Membership
# IMPORTANT: Maintain the MembershipSerializer Meta up to date return errors
# with this info (excluding there user_email and email)
read_only_fields = ("user",)
exclude = ("token",)
class _MemberBulkValidator(validators.Validator): class _MemberBulkValidator(validators.Validator):
email = serializers.EmailField() username = serializers.CharField()
role_id = serializers.IntegerField() role_id = serializers.IntegerField()
def validate_username(self, attrs, source):
username = attrs.get(source)
try:
validate_user_email_allowed_domains(username)
except InvalidEmailValidationError:
# If the validation comes from a request let's check the user is a valid contact
request = self.context.get("request", None)
if request is not None and request.user.is_authenticated():
valid_usernames = set(request.user.contacts_visible_by_user(request.user).values_list("username", flat=True))
if username not in valid_usernames:
raise ValidationError(_("The user must be a valid contact"))
return attrs
class MembersBulkValidator(ProjectExistsValidator, validators.Validator): class MembersBulkValidator(ProjectExistsValidator, validators.Validator):
project_id = serializers.IntegerField() project_id = serializers.IntegerField()
@ -192,12 +236,10 @@ class MembersBulkValidator(ProjectExistsValidator, validators.Validator):
invitation_extra_text = serializers.CharField(required=False, max_length=255) invitation_extra_text = serializers.CharField(required=False, max_length=255)
def validate_bulk_memberships(self, attrs, source): def validate_bulk_memberships(self, attrs, source):
filters = { project_id = attrs["project_id"]
"project__id": attrs["project_id"], role_ids = [r["role_id"] for r in attrs["bulk_memberships"]]
"id__in": [r["role_id"] for r in attrs["bulk_memberships"]]
}
if Role.objects.filter(**filters).count() != len(set(filters["id__in"])): if Role.objects.filter(project_id=project_id, id__in=role_ids).count() != len(set(role_ids)):
raise ValidationError(_("Invalid role ids. All roles must belong to the same project.")) raise ValidationError(_("Invalid role ids. All roles must belong to the same project."))
return attrs return attrs

View File

@ -46,17 +46,17 @@ from . import validators
from . import permissions from . import permissions
from . import filters as user_filters from . import filters as user_filters
from . import services from . import services
from . import utils as user_utils
from .signals import user_cancel_account as user_cancel_account_signal from .signals import user_cancel_account as user_cancel_account_signal
class UsersViewSet(ModelCrudViewSet): class UsersViewSet(ModelCrudViewSet):
permission_classes = (permissions.UserPermission,) permission_classes = (permissions.UserPermission,)
admin_serializer_class = serializers.UserAdminSerializer admin_serializer_class = serializers.UserAdminSerializer
serializer_class = serializers.UserSerializer serializer_class = serializers.UserSerializer
admin_validator_class = validators.UserAdminValidator admin_validator_class = validators.UserAdminValidator
validator_class = validators.UserValidator validator_class = validators.UserValidator
queryset = models.User.objects.all().prefetch_related("memberships")
filter_backends = (MembersFilterBackend,) filter_backends = (MembersFilterBackend,)
model = models.User
def get_serializer_class(self): def get_serializer_class(self):
if self.action in ["partial_update", "update", "retrieve", "by_username"]: if self.action in ["partial_update", "update", "retrieve", "by_username"]:
@ -74,6 +74,12 @@ class UsersViewSet(ModelCrudViewSet):
return self.validator_class return self.validator_class
def get_queryset(self):
qs = super().get_queryset()
qs = qs.prefetch_related("memberships")
qs = user_utils.attach_extra_info(qs, user=self.request.user)
return qs
def create(self, *args, **kwargs): def create(self, *args, **kwargs):
raise exc.NotSupported() raise exc.NotSupported()
@ -91,7 +97,7 @@ class UsersViewSet(ModelCrudViewSet):
return response.Ok(serializer.data) return response.Ok(serializer.data)
def retrieve(self, request, *args, **kwargs): def retrieve(self, request, *args, **kwargs):
self.object = get_object_or_404(models.User, **kwargs) self.object = get_object_or_404(self.get_queryset(), **kwargs)
self.check_permissions(request, 'retrieve', self.object) self.check_permissions(request, 'retrieve', self.object)
serializer = self.get_serializer(self.object) serializer = self.get_serializer(self.object)
return response.Ok(serializer.data) return response.Ok(serializer.data)

View File

@ -17,13 +17,24 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from taiga.base.filters import PermissionBasedFilterBackend from taiga.base.filters import PermissionBasedFilterBackend
from taiga.base.utils.db import to_tsquery
from . import services from . import services
class ContactsFilterBackend(PermissionBasedFilterBackend): class ContactsFilterBackend(PermissionBasedFilterBackend):
def filter_queryset(self, user, request, queryset, view): def filter_queryset(self, user, request, queryset, view):
qs = queryset.filter(is_active=True) qs = user.contacts_visible_by_user(request.user)
project_ids = services.get_visible_project_ids(user, request.user) q = request.QUERY_PARAMS.get('q', None)
qs = qs.filter(memberships__project_id__in=project_ids) if q:
qs = qs.exclude(id=user.id) table = qs.model._meta.db_table
where_clause = ("""
to_tsvector('english_nostop',
coalesce({table}.username, '') || ' ' ||
coalesce({table}.full_name) || ' ' ||
coalesce({table}.email, '')) @@ to_tsquery('english_nostop', %s)
""".format(table=table))
qs = qs.extra(where=[where_clause], params=[to_tsquery(q)])
return qs.distinct() return qs.distinct()

View File

@ -45,6 +45,8 @@ from taiga.permissions.choices import MEMBERS_PERMISSIONS
from taiga.projects.choices import BLOCKED_BY_OWNER_LEAVING from taiga.projects.choices import BLOCKED_BY_OWNER_LEAVING
from taiga.projects.notifications.choices import NotifyLevel from taiga.projects.notifications.choices import NotifyLevel
from . import services
def get_user_model_safe(): def get_user_model_safe():
""" """
@ -258,6 +260,13 @@ class User(AbstractBaseUser, PermissionsMixin):
def get_full_name(self): def get_full_name(self):
return self.full_name or self.username or self.email return self.full_name or self.username or self.email
def contacts_visible_by_user(self, user):
qs = User.objects.filter(is_active=True)
project_ids = services.get_visible_project_ids(self, user)
qs = qs.filter(memberships__project_id__in=project_ids)
qs = qs.exclude(id=self.id)
return qs
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
get_token_for_user(self, "cancel_account") get_token_for_user(self, "cancel_account")
super().save(*args, **kwargs) super().save(*args, **kwargs)

View File

@ -54,7 +54,6 @@ class UserSerializer(serializers.LightSerializer):
big_photo = MethodField() big_photo = MethodField()
gravatar_id = MethodField() gravatar_id = MethodField()
roles = MethodField() roles = MethodField()
projects_with_me = MethodField()
def get_full_name_display(self, obj): def get_full_name_display(self, obj):
return obj.get_full_name() if obj else "" return obj.get_full_name() if obj else ""
@ -69,21 +68,10 @@ class UserSerializer(serializers.LightSerializer):
return get_user_gravatar_id(user) return get_user_gravatar_id(user)
def get_roles(self, user): def get_roles(self, user):
return user.memberships. order_by("role__name").values_list("role__name", flat=True).distinct() if hasattr(user, "roles_attr"):
return user.roles_attr
def get_projects_with_me(self, user): return user.memberships.order_by("role__name").values_list("role__name", flat=True).distinct()
request = self.context.get("request", None)
requesting_user = request and request.user or None
if not requesting_user or not requesting_user.is_authenticated():
return []
else:
project_ids = requesting_user.memberships.values_list("project__id", flat=True)
memberships = user.memberships.filter(project__id__in=project_ids)
project_ids = memberships.values_list("project__id", flat=True)
projects = Project.objects.filter(id__in=project_ids)
return ContactProjectDetailSerializer(projects, many=True).data
class UserAdminSerializer(UserSerializer): class UserAdminSerializer(UserSerializer):

44
taiga/users/utils.py Normal file
View File

@ -0,0 +1,44 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2014-2016 Andrey Antukh <niwi@niwi.nz>
# Copyright (C) 2014-2016 Jesús Espino <jespinog@gmail.com>
# Copyright (C) 2014-2016 David Barragán <bameda@dbarragan.com>
# Copyright (C) 2014-2016 Alejandro Alonso <alejandro.alonso@kaleidos.net>
# Copyright (C) 2014-2016 Anler Hernández <hello@anler.me>
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
def attach_roles(queryset, as_field="roles_attr"):
"""Attach roles to each object of the queryset.
:param queryset: A Django user stories queryset object.
:param as_field: Attach the roles as an attribute with this name.
:return: Queryset object with the additional `as_field` field.
"""
model = queryset.model
sql = """SELECT ARRAY(
SELECT DISTINCT(users_role.name)
FROM projects_membership
INNER JOIN users_role ON users_role.id = projects_membership.role_id
WHERE projects_membership.user_id = {tbl}.id
ORDER BY users_role.name)
"""
sql = sql.format(tbl=model._meta.db_table)
queryset = queryset.extra(select={as_field: sql})
return queryset
def attach_extra_info(queryset, user=None):
queryset = attach_roles(queryset)
return queryset

View File

@ -1856,24 +1856,28 @@ def test_membership_update(client, data):
membership_data = serializers.MembershipSerializer(data.public_membership).data membership_data = serializers.MembershipSerializer(data.public_membership).data
membership_data["token"] = "test" membership_data["token"] = "test"
membership_data["username"] = data.public_membership.user.email
membership_data = json.dumps(membership_data) membership_data = json.dumps(membership_data)
results = helper_test_http_method(client, 'put', public_url, membership_data, users) results = helper_test_http_method(client, 'put', public_url, membership_data, users)
assert results == [401, 403, 403, 403, 200] assert results == [401, 403, 403, 403, 200]
membership_data = serializers.MembershipSerializer(data.private_membership1).data membership_data = serializers.MembershipSerializer(data.private_membership1).data
membership_data["token"] = "test" membership_data["token"] = "test"
membership_data["username"] = data.private_membership1.user.email
membership_data = json.dumps(membership_data) membership_data = json.dumps(membership_data)
results = helper_test_http_method(client, 'put', private1_url, membership_data, users) results = helper_test_http_method(client, 'put', private1_url, membership_data, users)
assert results == [401, 403, 403, 403, 200] assert results == [401, 403, 403, 403, 200]
membership_data = serializers.MembershipSerializer(data.private_membership2).data membership_data = serializers.MembershipSerializer(data.private_membership2).data
membership_data["token"] = "test" membership_data["token"] = "test"
membership_data["username"] = data.private_membership2.user.email
membership_data = json.dumps(membership_data) membership_data = json.dumps(membership_data)
results = helper_test_http_method(client, 'put', private2_url, membership_data, users) results = helper_test_http_method(client, 'put', private2_url, membership_data, users)
assert results == [401, 403, 403, 403, 200] assert results == [401, 403, 403, 403, 200]
membership_data = serializers.MembershipSerializer(data.blocked_membership).data membership_data = serializers.MembershipSerializer(data.blocked_membership).data
membership_data["token"] = "test" membership_data["token"] = "test"
membership_data["username"] = data.blocked_membership.user.email
membership_data = json.dumps(membership_data) membership_data = json.dumps(membership_data)
results = helper_test_http_method(client, 'put', blocked_url, membership_data, users) results = helper_test_http_method(client, 'put', blocked_url, membership_data, users)
assert results == [401, 403, 403, 403, 451] assert results == [401, 403, 403, 403, 451]
@ -1972,29 +1976,33 @@ def test_membership_create(client, data):
] ]
membership_data = serializers.MembershipSerializer(data.public_membership).data membership_data = serializers.MembershipSerializer(data.public_membership).data
membership_data["id"] = None del(membership_data["id"])
membership_data["email"] = "test1@test.com" del(membership_data["user"])
membership_data["username"] = "test1@test.com"
membership_data = json.dumps(membership_data) membership_data = json.dumps(membership_data)
results = helper_test_http_method(client, 'post', url, membership_data, users) results = helper_test_http_method(client, 'post', url, membership_data, users)
assert results == [401, 403, 403, 403, 201] assert results == [401, 403, 403, 403, 201]
membership_data = serializers.MembershipSerializer(data.private_membership1).data membership_data = serializers.MembershipSerializer(data.private_membership1).data
membership_data["id"] = None del(membership_data["id"])
membership_data["email"] = "test2@test.com" del(membership_data["user"])
membership_data["username"] = "test2@test.com"
membership_data = json.dumps(membership_data) membership_data = json.dumps(membership_data)
results = helper_test_http_method(client, 'post', url, membership_data, users) results = helper_test_http_method(client, 'post', url, membership_data, users)
assert results == [401, 403, 403, 403, 201] assert results == [401, 403, 403, 403, 201]
membership_data = serializers.MembershipSerializer(data.private_membership2).data membership_data = serializers.MembershipSerializer(data.private_membership2).data
membership_data["id"] = None del(membership_data["id"])
membership_data["email"] = "test3@test.com" del(membership_data["user"])
membership_data["username"] = "test3@test.com"
membership_data = json.dumps(membership_data) membership_data = json.dumps(membership_data)
results = helper_test_http_method(client, 'post', url, membership_data, users) results = helper_test_http_method(client, 'post', url, membership_data, users)
assert results == [401, 403, 403, 403, 201] assert results == [401, 403, 403, 403, 201]
membership_data = serializers.MembershipSerializer(data.blocked_membership).data membership_data = serializers.MembershipSerializer(data.blocked_membership).data
membership_data["id"] = None del(membership_data["id"])
membership_data["email"] = "test4@test.com" del(membership_data["user"])
membership_data["username"] = "test4@test.com"
membership_data = json.dumps(membership_data) membership_data = json.dumps(membership_data)
results = helper_test_http_method(client, 'post', url, membership_data, users) results = helper_test_http_method(client, 'post', url, membership_data, users)
assert results == [401, 403, 403, 403, 451] assert results == [401, 403, 403, 403, 451]
@ -2014,8 +2022,8 @@ def test_membership_action_bulk_create(client, data):
bulk_data = { bulk_data = {
"project_id": data.public_project.id, "project_id": data.public_project.id,
"bulk_memberships": [ "bulk_memberships": [
{"role_id": data.public_membership.role.pk, "email": "test1@test.com"}, {"role_id": data.public_membership.role.pk, "username": "test1@test.com"},
{"role_id": data.public_membership.role.pk, "email": "test2@test.com"}, {"role_id": data.public_membership.role.pk, "username": "test2@test.com"},
] ]
} }
bulk_data = json.dumps(bulk_data) bulk_data = json.dumps(bulk_data)
@ -2025,8 +2033,8 @@ def test_membership_action_bulk_create(client, data):
bulk_data = { bulk_data = {
"project_id": data.private_project1.id, "project_id": data.private_project1.id,
"bulk_memberships": [ "bulk_memberships": [
{"role_id": data.private_membership1.role.pk, "email": "test1@test.com"}, {"role_id": data.private_membership1.role.pk, "username": "test1@test.com"},
{"role_id": data.private_membership1.role.pk, "email": "test2@test.com"}, {"role_id": data.private_membership1.role.pk, "username": "test2@test.com"},
] ]
} }
bulk_data = json.dumps(bulk_data) bulk_data = json.dumps(bulk_data)
@ -2036,8 +2044,8 @@ def test_membership_action_bulk_create(client, data):
bulk_data = { bulk_data = {
"project_id": data.private_project2.id, "project_id": data.private_project2.id,
"bulk_memberships": [ "bulk_memberships": [
{"role_id": data.private_membership2.role.pk, "email": "test1@test.com"}, {"role_id": data.private_membership2.role.pk, "username": "test1@test.com"},
{"role_id": data.private_membership2.role.pk, "email": "test2@test.com"}, {"role_id": data.private_membership2.role.pk, "username": "test2@test.com"},
] ]
} }
bulk_data = json.dumps(bulk_data) bulk_data = json.dumps(bulk_data)
@ -2047,8 +2055,8 @@ def test_membership_action_bulk_create(client, data):
bulk_data = { bulk_data = {
"project_id": data.blocked_project.id, "project_id": data.blocked_project.id,
"bulk_memberships": [ "bulk_memberships": [
{"role_id": data.blocked_membership.role.pk, "email": "test1@test.com"}, {"role_id": data.blocked_membership.role.pk, "username": "test1@test.com"},
{"role_id": data.blocked_membership.role.pk, "email": "test2@test.com"}, {"role_id": data.blocked_membership.role.pk, "username": "test2@test.com"},
] ]
} }
bulk_data = json.dumps(bulk_data) bulk_data = json.dumps(bulk_data)

View File

@ -30,8 +30,8 @@ pytestmark = pytest.mark.django_db
def test_get_members_from_bulk(): def test_get_members_from_bulk():
data = [{"role_id": "1", "email": "member1@email.com"}, data = [{"role_id": "1", "username": "member1@email.com"},
{"role_id": "1", "email": "member2@email.com"}] {"role_id": "1", "username": "member2@email.com"}]
members = services.get_members_from_bulk(data, project_id=1) members = services.get_members_from_bulk(data, project_id=1)
assert len(members) == 2 assert len(members) == 2
@ -39,10 +39,65 @@ def test_get_members_from_bulk():
assert members[1].email == "member2@email.com" assert members[1].email == "member2@email.com"
def test_create_member_using_email(client):
project = f.ProjectFactory()
john = f.UserFactory.create()
joseph = f.UserFactory.create()
tester = f.RoleFactory(project=project, name="Tester")
f.MembershipFactory(project=project, user=john, is_admin=True)
url = reverse("memberships-list")
data = {"project": project.id, "role": tester.pk, "username": joseph.email}
client.login(john)
response = client.json.post(url, json.dumps(data))
assert response.status_code == 201
assert response.data["email"] == joseph.email
def test_create_member_using_username_without_being_contacts(client):
project = f.ProjectFactory()
john = f.UserFactory.create()
joseph = f.UserFactory.create()
tester = f.RoleFactory(project=project, name="Tester")
f.MembershipFactory(project=project, user=john, is_admin=True)
url = reverse("memberships-list")
data = {"project": project.id, "role": tester.pk, "username": joseph.username}
client.login(john)
response = client.json.post(url, json.dumps(data))
assert response.status_code == 400
assert "The user must be a valid contact" in response.data["username"][0]
def test_create_member_using_username_being_contacts(client):
project = f.ProjectFactory()
john = f.UserFactory.create()
joseph = f.UserFactory.create()
tester = f.RoleFactory(project=project, name="Tester", permissions=["view_project"])
f.MembershipFactory(project=project, user=john, role=tester, is_admin=True)
# They are members from another project
project2 = f.ProjectFactory()
gamer = f.RoleFactory(project=project2, name="Gamer", permissions=["view_project"])
f.MembershipFactory(project=project2, user=john, role=gamer, is_admin=True)
f.MembershipFactory(project=project2, user=joseph, role=gamer)
url = reverse("memberships-list")
data = {"project": project.id, "role": tester.pk, "username": joseph.username}
client.login(john)
response = client.json.post(url, json.dumps(data))
assert response.status_code == 201
assert response.data["user"] == joseph.id
def test_create_members_in_bulk(): def test_create_members_in_bulk():
with mock.patch("taiga.projects.services.members.db") as db: with mock.patch("taiga.projects.services.members.db") as db:
data = [{"role_id": "1", "email": "member1@email.com"}, data = [{"role_id": "1", "username": "member1@email.com"},
{"role_id": "1", "email": "member2@email.com"}] {"role_id": "1", "username": "member2@email.com"}]
members = services.create_members_in_bulk(data, project_id=1) members = services.create_members_in_bulk(data, project_id=1)
db.save_in_bulk.assert_called_once_with(members, None, None) db.save_in_bulk.assert_called_once_with(members, None, None)
@ -51,25 +106,57 @@ def test_api_create_bulk_members(client):
project = f.ProjectFactory() project = f.ProjectFactory()
john = f.UserFactory.create() john = f.UserFactory.create()
joseph = f.UserFactory.create() joseph = f.UserFactory.create()
tester = f.RoleFactory(project=project, name="Tester") other = f.UserFactory.create()
gamer = f.RoleFactory(project=project, name="Gamer") tester = f.RoleFactory(project=project, name="Tester", permissions=["view_project"])
f.MembershipFactory(project=project, user=project.owner, is_admin=True) gamer = f.RoleFactory(project=project, name="Gamer", permissions=["view_project"])
f.MembershipFactory(project=project, user=john, role=tester, is_admin=True)
# John and Other are members from another project
project2 = f.ProjectFactory()
f.MembershipFactory(project=project2, user=john, role=gamer, is_admin=True)
f.MembershipFactory(project=project2, user=other, role=gamer)
url = reverse("memberships-bulk-create") url = reverse("memberships-bulk-create")
data = { data = {
"project_id": project.id, "project_id": project.id,
"bulk_memberships": [ "bulk_memberships": [
{"role_id": tester.pk, "email": john.email}, {"role_id": gamer.pk, "username": joseph.email},
{"role_id": gamer.pk, "email": joseph.email}, {"role_id": gamer.pk, "username": other.username},
] ]
} }
client.login(project.owner) client.login(john)
response = client.json.post(url, json.dumps(data)) response = client.json.post(url, json.dumps(data))
assert response.status_code == 200 assert response.status_code == 200
assert response.data[0]["email"] == john.email response_user_ids = set([u["user"] for u in response.data])
assert response.data[1]["email"] == joseph.email user_ids = {other.id, joseph.id}
assert(user_ids.issubset(response_user_ids))
def test_api_create_bulk_members_invalid_user_id(client):
project = f.ProjectFactory()
john = f.UserFactory.create()
joseph = f.UserFactory.create()
other = f.UserFactory.create()
tester = f.RoleFactory(project=project, name="Tester", permissions=["view_project"])
gamer = f.RoleFactory(project=project, name="Gamer", permissions=["view_project"])
f.MembershipFactory(project=project, user=john, role=tester, is_admin=True)
url = reverse("memberships-bulk-create")
data = {
"project_id": project.id,
"bulk_memberships": [
{"role_id": gamer.pk, "username": joseph.email},
{"role_id": gamer.pk, "username": other.username},
]
}
client.login(john)
response = client.json.post(url, json.dumps(data))
assert response.status_code == 400
test_api_create_bulk_members_invalid_user_id
def test_api_create_bulk_members_with_invalid_roles(client): def test_api_create_bulk_members_with_invalid_roles(client):
@ -85,8 +172,8 @@ def test_api_create_bulk_members_with_invalid_roles(client):
data = { data = {
"project_id": project.id, "project_id": project.id,
"bulk_memberships": [ "bulk_memberships": [
{"role_id": tester.pk, "email": john.email}, {"role_id": tester.pk, "username": john.email},
{"role_id": gamer.pk, "email": joseph.email}, {"role_id": gamer.pk, "username": joseph.email},
] ]
} }
client.login(project.owner) client.login(project.owner)
@ -109,8 +196,8 @@ def test_api_create_bulk_members_with_allowed_domain(client):
data = { data = {
"project_id": project.id, "project_id": project.id,
"bulk_memberships": [ "bulk_memberships": [
{"role_id": tester.pk, "email": "test1@email.com"}, {"role_id": tester.pk, "username": "test1@email.com"},
{"role_id": gamer.pk, "email": "test2@email.com"}, {"role_id": gamer.pk, "username": "test2@email.com"},
] ]
} }
client.login(project.owner) client.login(project.owner)
@ -133,16 +220,17 @@ def test_api_create_bulk_members_with_allowed_and_unallowed_domain(client, setti
data = { data = {
"project_id": project.id, "project_id": project.id,
"bulk_memberships": [ "bulk_memberships": [
{"role_id": tester.pk, "email": "test@invalid-domain.com"}, {"role_id": tester.pk, "username": "test@invalid-domain.com"},
{"role_id": gamer.pk, "email": "test@email.com"}, {"role_id": gamer.pk, "username": "test@email.com"},
] ]
} }
client.login(project.owner) client.login(project.owner)
response = client.json.post(url, json.dumps(data)) response = client.json.post(url, json.dumps(data))
print(response.data)
assert response.status_code == 400 assert response.status_code == 400
assert "email" in response.data["bulk_memberships"][0] assert "username" in response.data["bulk_memberships"][0]
assert "email" not in response.data["bulk_memberships"][1] assert "username" not in response.data["bulk_memberships"][1]
def test_api_create_bulk_members_with_unallowed_domains(client, settings): def test_api_create_bulk_members_with_unallowed_domains(client, settings):
@ -157,16 +245,16 @@ def test_api_create_bulk_members_with_unallowed_domains(client, settings):
data = { data = {
"project_id": project.id, "project_id": project.id,
"bulk_memberships": [ "bulk_memberships": [
{"role_id": tester.pk, "email": "test1@invalid-domain.com"}, {"role_id": tester.pk, "username": "test1@invalid-domain.com"},
{"role_id": gamer.pk, "email": "test2@invalid-domain.com"}, {"role_id": gamer.pk, "username": "test2@invalid-domain.com"},
] ]
} }
client.login(project.owner) client.login(project.owner)
response = client.json.post(url, json.dumps(data)) response = client.json.post(url, json.dumps(data))
assert response.status_code == 400 assert response.status_code == 400
assert "email" in response.data["bulk_memberships"][0] assert "username" in response.data["bulk_memberships"][0]
assert "email" in response.data["bulk_memberships"][1] assert "username" in response.data["bulk_memberships"][1]
def test_api_create_bulk_members_without_enough_memberships_private_project_slots_one_project(client): def test_api_create_bulk_members_without_enough_memberships_private_project_slots_one_project(client):
@ -180,10 +268,10 @@ def test_api_create_bulk_members_without_enough_memberships_private_project_slot
data = { data = {
"project_id": project.id, "project_id": project.id,
"bulk_memberships": [ "bulk_memberships": [
{"role_id": role.pk, "email": "test1@test.com"}, {"role_id": role.pk, "username": "test1@test.com"},
{"role_id": role.pk, "email": "test2@test.com"}, {"role_id": role.pk, "username": "test2@test.com"},
{"role_id": role.pk, "email": "test3@test.com"}, {"role_id": role.pk, "username": "test3@test.com"},
{"role_id": role.pk, "email": "test4@test.com"}, {"role_id": role.pk, "username": "test4@test.com"},
] ]
} }
client.login(user) client.login(user)
@ -206,10 +294,10 @@ def test_api_create_bulk_members_for_admin_without_enough_memberships_private_pr
data = { data = {
"project_id": project.id, "project_id": project.id,
"bulk_memberships": [ "bulk_memberships": [
{"role_id": role.pk, "email": "test1@test.com"}, {"role_id": role.pk, "username": "test1@test.com"},
{"role_id": role.pk, "email": "test2@test.com"}, {"role_id": role.pk, "username": "test2@test.com"},
{"role_id": role.pk, "email": "test3@test.com"}, {"role_id": role.pk, "username": "test3@test.com"},
{"role_id": role.pk, "email": "test4@test.com"}, {"role_id": role.pk, "username": "test4@test.com"},
] ]
} }
client.login(user) client.login(user)
@ -237,10 +325,10 @@ def test_api_create_bulk_members_with_enough_memberships_private_project_slots_m
data = { data = {
"project_id": project.id, "project_id": project.id,
"bulk_memberships": [ "bulk_memberships": [
{"role_id": role.pk, "email": "test1@test.com"}, {"role_id": role.pk, "username": "test1@test.com"},
{"role_id": role.pk, "email": "test2@test.com"}, {"role_id": role.pk, "username": "test2@test.com"},
{"role_id": role.pk, "email": "test3@test.com"}, {"role_id": role.pk, "username": "test3@test.com"},
{"role_id": role.pk, "email": "test4@test.com"}, {"role_id": role.pk, "username": "test4@test.com"},
] ]
} }
client.login(user) client.login(user)
@ -260,10 +348,10 @@ def test_api_create_bulk_members_without_enough_memberships_public_project_slots
data = { data = {
"project_id": project.id, "project_id": project.id,
"bulk_memberships": [ "bulk_memberships": [
{"role_id": role.pk, "email": "test1@test.com"}, {"role_id": role.pk, "username": "test1@test.com"},
{"role_id": role.pk, "email": "test2@test.com"}, {"role_id": role.pk, "username": "test2@test.com"},
{"role_id": role.pk, "email": "test3@test.com"}, {"role_id": role.pk, "username": "test3@test.com"},
{"role_id": role.pk, "email": "test4@test.com"}, {"role_id": role.pk, "username": "test4@test.com"},
] ]
} }
client.login(user) client.login(user)
@ -290,10 +378,10 @@ def test_api_create_bulk_members_with_enough_memberships_public_project_slots_mu
data = { data = {
"project_id": project.id, "project_id": project.id,
"bulk_memberships": [ "bulk_memberships": [
{"role_id": role.pk, "email": "test1@test.com"}, {"role_id": role.pk, "username": "test1@test.com"},
{"role_id": role.pk, "email": "test2@test.com"}, {"role_id": role.pk, "username": "test2@test.com"},
{"role_id": role.pk, "email": "test3@test.com"}, {"role_id": role.pk, "username": "test3@test.com"},
{"role_id": role.pk, "email": "test4@test.com"}, {"role_id": role.pk, "username": "test4@test.com"},
] ]
} }
client.login(user) client.login(user)
@ -312,7 +400,7 @@ def test_api_create_bulk_members_with_extra_text(client, outbox):
data = { data = {
"project_id": project.id, "project_id": project.id,
"bulk_memberships": [ "bulk_memberships": [
{"role_id": tester.pk, "email": "john@email.com"}, {"role_id": tester.pk, "username": "john@email.com"},
], ],
"invitation_extra_text": invitation_extra_text "invitation_extra_text": invitation_extra_text
} }
@ -350,7 +438,7 @@ def test_api_invite_existing_user(client, outbox):
client.login(role.project.owner) client.login(role.project.owner)
url = reverse("memberships-list") url = reverse("memberships-list")
data = {"role": role.pk, "project": role.project.pk, "email": user.email} data = {"role": role.pk, "project": role.project.pk, "username": user.email}
response = client.json.post(url, json.dumps(data)) response = client.json.post(url, json.dumps(data))
@ -364,7 +452,7 @@ def test_api_invite_existing_user(client, outbox):
assert "Added to the project" in message.subject assert "Added to the project" in message.subject
def test_api_create_invalid_membership_email_failing(client): def test_api_create_invalid_membership_no_email_no_user(client):
"Should not create the invitation linked to that user" "Should not create the invitation linked to that user"
user = f.UserFactory.create() user = f.UserFactory.create()
role = f.RoleFactory.create() role = f.RoleFactory.create()
@ -388,7 +476,7 @@ def test_api_create_invalid_membership_role_doesnt_exist_in_the_project(client):
client.login(project.owner) client.login(project.owner)
url = reverse("memberships-list") url = reverse("memberships-list")
data = {"role": role.pk, "project": project.pk, "email": user.email} data = {"role": role.pk, "project": project.pk, "username": user.email}
response = client.json.post(url, json.dumps(data)) response = client.json.post(url, json.dumps(data))
@ -404,7 +492,7 @@ def test_api_create_membership(client):
client.login(membership.user) client.login(membership.user)
url = reverse("memberships-list") url = reverse("memberships-list")
data = {"role": role.pk, "project": role.project.pk, "email": user.email} data = {"role": role.pk, "project": role.project.pk, "username": user.email}
response = client.json.post(url, json.dumps(data)) response = client.json.post(url, json.dumps(data))
assert response.status_code == 201 assert response.status_code == 201
@ -419,11 +507,11 @@ def test_api_create_membership_with_unallowed_domain(client, settings):
client.login(membership.user) client.login(membership.user)
url = reverse("memberships-list") url = reverse("memberships-list")
data = {"role": role.pk, "project": role.project.pk, "email": "test@invalid-email.com"} data = {"role": role.pk, "project": role.project.pk, "username": "test@invalid-email.com"}
response = client.json.post(url, json.dumps(data)) response = client.json.post(url, json.dumps(data))
assert response.status_code == 400 assert response.status_code == 400
assert "email" in response.data assert "username" in response.data
def test_api_create_membership_with_allowed_domain(client, settings): def test_api_create_membership_with_allowed_domain(client, settings):
@ -434,7 +522,7 @@ def test_api_create_membership_with_allowed_domain(client, settings):
client.login(membership.user) client.login(membership.user)
url = reverse("memberships-list") url = reverse("memberships-list")
data = {"role": role.pk, "project": role.project.pk, "email": "test@email.com"} data = {"role": role.pk, "project": role.project.pk, "username": "test@email.com"}
response = client.json.post(url, json.dumps(data)) response = client.json.post(url, json.dumps(data))
assert response.status_code == 201 assert response.status_code == 201
@ -449,7 +537,7 @@ def test_api_create_membership_without_enough_memberships_private_project_slots_
client.login(user) client.login(user)
url = reverse("memberships-list") url = reverse("memberships-list")
data = {"role": role.pk, "project": project.pk, "email": "test@test.com"} data = {"role": role.pk, "project": project.pk, "username": "test@test.com"}
response = client.json.post(url, json.dumps(data)) response = client.json.post(url, json.dumps(data))
assert response.status_code == 400 assert response.status_code == 400
@ -470,7 +558,7 @@ def test_api_create_membership_with_enough_memberships_private_project_slots_mul
client.login(user) client.login(user)
url = reverse("memberships-list") url = reverse("memberships-list")
data = {"role": role.pk, "project": project.pk, "email": "test@test.com"} data = {"role": role.pk, "project": project.pk, "username": "test@test.com"}
response = client.json.post(url, json.dumps(data)) response = client.json.post(url, json.dumps(data))
assert response.status_code == 201 assert response.status_code == 201
@ -484,7 +572,7 @@ def test_api_create_membership_without_enough_memberships_public_project_slots_o
client.login(user) client.login(user)
url = reverse("memberships-list") url = reverse("memberships-list")
data = {"role": role.pk, "project": project.pk, "email": "test@test.com"} data = {"role": role.pk, "project": project.pk, "username": "test@test.com"}
response = client.json.post(url, json.dumps(data)) response = client.json.post(url, json.dumps(data))
assert response.status_code == 400 assert response.status_code == 400
@ -505,7 +593,7 @@ def test_api_create_membership_with_enough_memberships_public_project_slots_mult
client.login(user) client.login(user)
url = reverse("memberships-list") url = reverse("memberships-list")
data = {"role": role.pk, "project": project.pk, "email": "test@test.com"} data = {"role": role.pk, "project": project.pk, "username": "test@test.com"}
response = client.json.post(url, json.dumps(data)) response = client.json.post(url, json.dumps(data))
assert response.status_code == 201 assert response.status_code == 201
@ -515,11 +603,20 @@ def test_api_edit_membership(client):
membership = f.MembershipFactory(is_admin=True) membership = f.MembershipFactory(is_admin=True)
client.login(membership.user) client.login(membership.user)
url = reverse("memberships-detail", args=[membership.id]) url = reverse("memberships-detail", args=[membership.id])
data = {"email": "new@email.com"} data = {"username": "new@email.com"}
response = client.json.patch(url, json.dumps(data)) response = client.json.patch(url, json.dumps(data))
assert response.status_code == 200 assert response.status_code == 200
def test_api_edit_membership(client):
membership = f.MembershipFactory(is_admin=True)
client.login(membership.user)
url = reverse("memberships-detail", args=[membership.id])
data = {"username": "new@email.com"}
response = client.json.patch(url, json.dumps(data))
assert response.status_code == 200
def test_api_change_owner_membership_to_no_admin_return_error(client): def test_api_change_owner_membership_to_no_admin_return_error(client):
project = f.ProjectFactory() project = f.ProjectFactory()
membership_owner = f.MembershipFactory(project=project, user=project.owner, is_admin=True) membership_owner = f.MembershipFactory(project=project, user=project.owner, is_admin=True)