Merge pull request #312 from taigaio/user-contacts-API
User contacts APIremotes/origin/enhancement/email-actions
commit
f0d53c7bd7
|
@ -44,6 +44,7 @@ from djmail.template_mail import InlineCSSTemplateMail
|
||||||
from . import models
|
from . import models
|
||||||
from . import serializers
|
from . import serializers
|
||||||
from . import permissions
|
from . import permissions
|
||||||
|
from . import filters as user_filters
|
||||||
from .signals import user_cancel_account as user_cancel_account_signal
|
from .signals import user_cancel_account as user_cancel_account_signal
|
||||||
|
|
||||||
|
|
||||||
|
@ -51,7 +52,7 @@ 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
|
||||||
queryset = models.User.objects.all()
|
queryset = models.User.objects.all().prefetch_related("memberships")
|
||||||
filter_backends = (MembersFilterBackend,)
|
filter_backends = (MembersFilterBackend,)
|
||||||
|
|
||||||
def get_serializer_class(self):
|
def get_serializer_class(self):
|
||||||
|
@ -77,6 +78,23 @@ class UsersViewSet(ModelCrudViewSet):
|
||||||
|
|
||||||
return response.Ok(serializer.data)
|
return response.Ok(serializer.data)
|
||||||
|
|
||||||
|
@detail_route(methods=["GET"])
|
||||||
|
def contacts(self, request, *args, **kwargs):
|
||||||
|
user = self.get_object()
|
||||||
|
self.check_permissions(request, 'contacts', user)
|
||||||
|
|
||||||
|
self.object_list = user_filters.ContactsFilterBackend().filter_queryset(request,
|
||||||
|
self.get_queryset(),
|
||||||
|
self)
|
||||||
|
|
||||||
|
page = self.paginate_queryset(self.object_list)
|
||||||
|
if page is not None:
|
||||||
|
serializer = self.serializer_class(page.object_list, many=True)
|
||||||
|
else:
|
||||||
|
serializer = self.serializer_class(self.object_list, many=True)
|
||||||
|
|
||||||
|
return response.Ok(serializer.data)
|
||||||
|
|
||||||
@list_route(methods=["POST"])
|
@list_route(methods=["POST"])
|
||||||
def password_recovery(self, request, pk=None):
|
def password_recovery(self, request, pk=None):
|
||||||
username_or_email = request.DATA.get('username', None)
|
username_or_email = request.DATA.get('username', None)
|
||||||
|
|
|
@ -0,0 +1,46 @@
|
||||||
|
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be>
|
||||||
|
# Copyright (C) 2014 Jesús Espino <jespinog@gmail.com>
|
||||||
|
# Copyright (C) 2014 David Barragán <bameda@dbarragan.com>
|
||||||
|
# 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/>.
|
||||||
|
|
||||||
|
from django.apps import apps
|
||||||
|
from django.db.models import Q
|
||||||
|
|
||||||
|
from taiga.base.filters import PermissionBasedFilterBackend
|
||||||
|
|
||||||
|
class ContactsFilterBackend(PermissionBasedFilterBackend):
|
||||||
|
permission = "view_project"
|
||||||
|
|
||||||
|
def filter_queryset(self, request, queryset, view):
|
||||||
|
qs = queryset.filter(is_active=True)
|
||||||
|
|
||||||
|
# Authenticated
|
||||||
|
if request.user.is_authenticated():
|
||||||
|
# if super user we don't need to filter anything
|
||||||
|
if not request.user.is_superuser:
|
||||||
|
Membership = apps.get_model('projects', 'Membership')
|
||||||
|
memberships_qs = Membership.objects.filter(user=request.user)
|
||||||
|
memberships_qs = memberships_qs.filter(Q(role__permissions__contains=[self.permission]) |
|
||||||
|
Q(is_owner=True))
|
||||||
|
|
||||||
|
projects_list = [membership.project_id for membership in memberships_qs]
|
||||||
|
qs = qs.filter(memberships__project_id__in=projects_list)
|
||||||
|
|
||||||
|
qs = qs.exclude(id=request.user.id)
|
||||||
|
|
||||||
|
# Anonymous
|
||||||
|
else:
|
||||||
|
qs = qs.filter(memberships__project__anon_permissions__contains=[self.permission])
|
||||||
|
|
||||||
|
return qs.distinct()
|
|
@ -43,6 +43,7 @@ class UserPermission(TaigaResourcePermission):
|
||||||
remove_avatar_perms = IsAuthenticated()
|
remove_avatar_perms = IsAuthenticated()
|
||||||
starred_perms = AllowAny()
|
starred_perms = AllowAny()
|
||||||
change_email_perms = IsTheSameUser()
|
change_email_perms = IsTheSameUser()
|
||||||
|
contacts_perms = AllowAny()
|
||||||
|
|
||||||
|
|
||||||
class RolesPermission(TaigaResourcePermission):
|
class RolesPermission(TaigaResourcePermission):
|
||||||
|
|
|
@ -20,7 +20,7 @@ from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from taiga.base.api import serializers
|
from taiga.base.api import serializers
|
||||||
from taiga.base.fields import PgArrayField
|
from taiga.base.fields import PgArrayField
|
||||||
|
from taiga.projects.models import Project
|
||||||
from .models import User, Role
|
from .models import User, Role
|
||||||
from .services import get_photo_or_gravatar_url, get_big_photo_or_gravatar_url
|
from .services import get_photo_or_gravatar_url, get_big_photo_or_gravatar_url
|
||||||
|
|
||||||
|
@ -31,10 +31,18 @@ import re
|
||||||
## User
|
## User
|
||||||
######################################################
|
######################################################
|
||||||
|
|
||||||
|
class ContactProjectDetailSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = Project
|
||||||
|
fields = ("id", "slug", "name")
|
||||||
|
|
||||||
|
|
||||||
class UserSerializer(serializers.ModelSerializer):
|
class UserSerializer(serializers.ModelSerializer):
|
||||||
full_name_display = serializers.SerializerMethodField("get_full_name_display")
|
full_name_display = serializers.SerializerMethodField("get_full_name_display")
|
||||||
photo = serializers.SerializerMethodField("get_photo")
|
photo = serializers.SerializerMethodField("get_photo")
|
||||||
big_photo = serializers.SerializerMethodField("get_big_photo")
|
big_photo = serializers.SerializerMethodField("get_big_photo")
|
||||||
|
roles = serializers.SerializerMethodField("get_roles")
|
||||||
|
projects_with_me = serializers.SerializerMethodField("get_projects_with_me")
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = User
|
model = User
|
||||||
|
@ -42,7 +50,7 @@ class UserSerializer(serializers.ModelSerializer):
|
||||||
# with this info (including there the email)
|
# with this info (including there the email)
|
||||||
fields = ("id", "username", "full_name", "full_name_display",
|
fields = ("id", "username", "full_name", "full_name_display",
|
||||||
"color", "bio", "lang", "timezone", "is_active",
|
"color", "bio", "lang", "timezone", "is_active",
|
||||||
"photo", "big_photo")
|
"photo", "big_photo", "roles", "projects_with_me")
|
||||||
read_only_fields = ("id",)
|
read_only_fields = ("id",)
|
||||||
|
|
||||||
def validate_username(self, attrs, source):
|
def validate_username(self, attrs, source):
|
||||||
|
@ -72,6 +80,22 @@ class UserSerializer(serializers.ModelSerializer):
|
||||||
def get_big_photo(self, user):
|
def get_big_photo(self, user):
|
||||||
return get_big_photo_or_gravatar_url(user)
|
return get_big_photo_or_gravatar_url(user)
|
||||||
|
|
||||||
|
def get_roles(self, user):
|
||||||
|
return user.memberships. order_by("role__name").values_list("role__name", flat=True).distinct()
|
||||||
|
|
||||||
|
def get_projects_with_me(self, user):
|
||||||
|
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 = requesting_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):
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
|
@ -8,6 +8,7 @@ from .. import factories as f
|
||||||
from taiga.base.utils import json
|
from taiga.base.utils import json
|
||||||
from taiga.users import models
|
from taiga.users import models
|
||||||
from taiga.auth.tokens import get_token_for_user
|
from taiga.auth.tokens import get_token_for_user
|
||||||
|
from taiga.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS, USER_PERMISSIONS
|
||||||
|
|
||||||
pytestmark = pytest.mark.django_db
|
pytestmark = pytest.mark.django_db
|
||||||
|
|
||||||
|
@ -176,3 +177,63 @@ def test_change_avatar(client):
|
||||||
avatar.seek(0)
|
avatar.seek(0)
|
||||||
response = client.post(url, post_data)
|
response = client.post(url, post_data)
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
|
||||||
|
def test_list_contacts_private_projects(client):
|
||||||
|
project = f.ProjectFactory.create()
|
||||||
|
user_1 = f.UserFactory.create()
|
||||||
|
user_2 = f.UserFactory.create()
|
||||||
|
role = f.RoleFactory(project=project, permissions=["view_project"])
|
||||||
|
membership_1 = f.MembershipFactory.create(project=project, user=user_1, role=role)
|
||||||
|
membership_2 = f.MembershipFactory.create(project=project, user=user_2, role=role)
|
||||||
|
|
||||||
|
url = reverse('users-contacts', kwargs={"pk": user_1.pk})
|
||||||
|
response = client.get(url, content_type="application/json")
|
||||||
|
assert response.status_code == 404
|
||||||
|
|
||||||
|
client.login(user_1)
|
||||||
|
response = client.get(url, content_type="application/json")
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
response_content = json.loads(response.content.decode("utf-8"))
|
||||||
|
assert len(response_content) == 1
|
||||||
|
assert response_content[0]["id"] == user_2.id
|
||||||
|
|
||||||
|
|
||||||
|
def test_list_contacts_no_projects(client):
|
||||||
|
user_1 = f.UserFactory.create()
|
||||||
|
user_2 = f.UserFactory.create()
|
||||||
|
role_1 = f.RoleFactory(permissions=["view_project"])
|
||||||
|
role_2 = f.RoleFactory(permissions=["view_project"])
|
||||||
|
membership_1 = f.MembershipFactory.create(project=role_1.project, user=user_1, role=role_1)
|
||||||
|
membership_2 = f.MembershipFactory.create(project=role_2.project, user=user_2, role=role_2)
|
||||||
|
|
||||||
|
client.login(user_1)
|
||||||
|
|
||||||
|
url = reverse('users-contacts', kwargs={"pk": user_1.pk})
|
||||||
|
response = client.get(url, content_type="application/json")
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
response_content = json.loads(response.content.decode("utf-8"))
|
||||||
|
assert len(response_content) == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_list_contacts_public_projects(client):
|
||||||
|
project = f.ProjectFactory.create(is_private=False,
|
||||||
|
anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)),
|
||||||
|
public_permissions=list(map(lambda x: x[0], USER_PERMISSIONS)))
|
||||||
|
|
||||||
|
user_1 = f.UserFactory.create()
|
||||||
|
user_2 = f.UserFactory.create()
|
||||||
|
role = f.RoleFactory(project=project)
|
||||||
|
membership_1 = f.MembershipFactory.create(project=project, user=user_1, role=role)
|
||||||
|
membership_2 = f.MembershipFactory.create(project=project, user=user_2, role=role)
|
||||||
|
|
||||||
|
url = reverse('users-contacts', kwargs={"pk": user_1.pk})
|
||||||
|
response = client.get(url, content_type="application/json")
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
response_content = json.loads(response.content.decode("utf-8"))
|
||||||
|
assert len(response_content) == 2
|
||||||
|
assert response_content[0]["id"] == user_1.id
|
||||||
|
assert response_content[1]["id"] == user_2.id
|
||||||
|
|
Loading…
Reference in New Issue