Merge pull request #312 from taigaio/user-contacts-API

User contacts API
remotes/origin/enhancement/email-actions
David Barragán Merino 2015-04-28 14:19:02 +02:00
commit f0d53c7bd7
5 changed files with 153 additions and 3 deletions

View File

@ -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)

46
taiga/users/filters.py Normal file
View File

@ -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()

View File

@ -43,6 +43,7 @@ class UserPermission(TaigaResourcePermission):
remove_avatar_perms = IsAuthenticated()
starred_perms = AllowAny()
change_email_perms = IsTheSameUser()
contacts_perms = AllowAny()
class RolesPermission(TaigaResourcePermission):

View File

@ -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:

View File

@ -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