From 30a5cb3faa9b6da131a7ea2fd0b732f1f1202870 Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Fri, 24 Apr 2015 14:51:47 +0200 Subject: [PATCH] User contacts API --- taiga/users/api.py | 20 ++++++++++- taiga/users/filters.py | 46 +++++++++++++++++++++++++ taiga/users/permissions.py | 1 + taiga/users/serializers.py | 28 +++++++++++++-- tests/integration/test_users.py | 61 +++++++++++++++++++++++++++++++++ 5 files changed, 153 insertions(+), 3 deletions(-) create mode 100644 taiga/users/filters.py diff --git a/taiga/users/api.py b/taiga/users/api.py index 9265865d..ae2464e5 100644 --- a/taiga/users/api.py +++ b/taiga/users/api.py @@ -44,6 +44,7 @@ from djmail.template_mail import InlineCSSTemplateMail from . import models from . import serializers from . import permissions +from . import filters as user_filters from .signals import user_cancel_account as user_cancel_account_signal @@ -51,7 +52,7 @@ class UsersViewSet(ModelCrudViewSet): permission_classes = (permissions.UserPermission,) admin_serializer_class = serializers.UserAdminSerializer serializer_class = serializers.UserSerializer - queryset = models.User.objects.all() + queryset = models.User.objects.all().prefetch_related("memberships") filter_backends = (MembersFilterBackend,) def get_serializer_class(self): @@ -77,6 +78,23 @@ class UsersViewSet(ModelCrudViewSet): 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"]) def password_recovery(self, request, pk=None): username_or_email = request.DATA.get('username', None) diff --git a/taiga/users/filters.py b/taiga/users/filters.py new file mode 100644 index 00000000..b2f46a83 --- /dev/null +++ b/taiga/users/filters.py @@ -0,0 +1,46 @@ +# Copyright (C) 2014 Andrey Antukh +# Copyright (C) 2014 Jesús Espino +# Copyright (C) 2014 David Barragán +# 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 . + +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() diff --git a/taiga/users/permissions.py b/taiga/users/permissions.py index cbabe22c..6a36ab4a 100644 --- a/taiga/users/permissions.py +++ b/taiga/users/permissions.py @@ -43,6 +43,7 @@ class UserPermission(TaigaResourcePermission): remove_avatar_perms = IsAuthenticated() starred_perms = AllowAny() change_email_perms = IsTheSameUser() + contacts_perms = AllowAny() class RolesPermission(TaigaResourcePermission): diff --git a/taiga/users/serializers.py b/taiga/users/serializers.py index 9b09ac25..244d9218 100644 --- a/taiga/users/serializers.py +++ b/taiga/users/serializers.py @@ -20,7 +20,7 @@ from django.utils.translation import ugettext_lazy as _ from taiga.base.api import serializers from taiga.base.fields import PgArrayField - +from taiga.projects.models import Project from .models import User, Role from .services import get_photo_or_gravatar_url, get_big_photo_or_gravatar_url @@ -31,10 +31,18 @@ import re ## User ###################################################### +class ContactProjectDetailSerializer(serializers.ModelSerializer): + class Meta: + model = Project + fields = ("id", "slug", "name") + + class UserSerializer(serializers.ModelSerializer): full_name_display = serializers.SerializerMethodField("get_full_name_display") photo = serializers.SerializerMethodField("get_photo") big_photo = serializers.SerializerMethodField("get_big_photo") + roles = serializers.SerializerMethodField("get_roles") + projects_with_me = serializers.SerializerMethodField("get_projects_with_me") class Meta: model = User @@ -42,7 +50,7 @@ class UserSerializer(serializers.ModelSerializer): # with this info (including there the email) fields = ("id", "username", "full_name", "full_name_display", "color", "bio", "lang", "timezone", "is_active", - "photo", "big_photo") + "photo", "big_photo", "roles", "projects_with_me") read_only_fields = ("id",) def validate_username(self, attrs, source): @@ -72,6 +80,22 @@ class UserSerializer(serializers.ModelSerializer): def get_big_photo(self, 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 Meta: diff --git a/tests/integration/test_users.py b/tests/integration/test_users.py index 60f2fcc7..4e42968b 100644 --- a/tests/integration/test_users.py +++ b/tests/integration/test_users.py @@ -8,6 +8,7 @@ from .. import factories as f from taiga.base.utils import json from taiga.users import models 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 @@ -176,3 +177,63 @@ def test_change_avatar(client): avatar.seek(0) response = client.post(url, post_data) 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