User favourites API

remotes/origin/enhancement/email-actions
Alejandro Alonso 2015-08-18 09:23:20 +02:00 committed by David Barragán Merino
parent bccdc2fae1
commit 3492b46cc9
6 changed files with 432 additions and 1 deletions

View File

@ -114,6 +114,33 @@ class UsersViewSet(ModelCrudViewSet):
self.check_permissions(request, "stats", user) self.check_permissions(request, "stats", user)
return response.Ok(services.get_stats_for_user(user, request.user)) return response.Ok(services.get_stats_for_user(user, request.user))
@detail_route(methods=["GET"])
def favourites(self, request, *args, **kwargs):
for_user = get_object_or_404(models.User, **kwargs)
from_user = request.user
self.check_permissions(request, 'favourites', for_user)
filters = {
"type": request.GET.get("type", None),
"action": request.GET.get("action", None),
"q": request.GET.get("q", None),
}
self.object_list = services.get_favourites_list(for_user, from_user, **filters)
page = self.paginate_queryset(self.object_list)
extra_args = {
"many": True,
"user_votes": services.get_voted_content_for_user(request.user),
"user_watching": services.get_watched_content_for_user(request.user),
}
if page is not None:
serializer = serializers.FavouriteSerializer(page.object_list, **extra_args)
else:
serializer = serializers.FavouriteSerializer(self.object_list, **extra_args)
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)

View File

@ -47,6 +47,7 @@ class UserPermission(TaigaResourcePermission):
starred_perms = AllowAny() starred_perms = AllowAny()
change_email_perms = AllowAny() change_email_perms = AllowAny()
contacts_perms = AllowAny() contacts_perms = AllowAny()
favourites_perms = AllowAny()
class RolesPermission(TaigaResourcePermission): class RolesPermission(TaigaResourcePermission):

View File

@ -19,11 +19,14 @@ from django.core.exceptions import ValidationError
from django.utils.translation import ugettext_lazy as _ 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, TagsField
from taiga.projects.models import Project 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
from collections import namedtuple
import re import re
@ -149,3 +152,55 @@ class ProjectRoleSerializer(serializers.ModelSerializer):
model = Role model = Role
fields = ('id', 'name', 'slug', 'order', 'computable') fields = ('id', 'name', 'slug', 'order', 'computable')
i18n_fields = ("name",) i18n_fields = ("name",)
######################################################
## Favourite
######################################################
class FavouriteSerializer(serializers.Serializer):
type = serializers.CharField()
action = serializers.CharField()
id = serializers.IntegerField()
ref = serializers.IntegerField()
slug = serializers.CharField()
subject = serializers.CharField()
tags = TagsField(default=[])
project = serializers.IntegerField()
assigned_to = serializers.IntegerField()
total_watchers = serializers.IntegerField()
is_voted = serializers.SerializerMethodField("get_is_voted")
is_watched = serializers.SerializerMethodField("get_is_watched")
created_date = serializers.DateTimeField()
project_name = serializers.CharField()
project_slug = serializers.CharField()
project_is_private = serializers.CharField()
assigned_to_username = serializers.CharField()
assigned_to_full_name = serializers.CharField()
assigned_to_photo = serializers.SerializerMethodField("get_photo")
total_votes = serializers.IntegerField()
def __init__(self, *args, **kwargs):
# Don't pass the extra ids args up to the superclass
self.user_votes = kwargs.pop("user_votes", {})
self.user_watching = kwargs.pop("user_watching", {})
# Instantiate the superclass normally
super(FavouriteSerializer, self).__init__(*args, **kwargs)
def get_is_voted(self, obj):
return obj["id"] in self.user_votes.get(obj["type"], [])
def get_is_watched(self, obj):
return obj["id"] in self.user_watching.get(obj["type"], [])
def get_photo(self, obj):
UserData = namedtuple("UserData", ["photo", "email"])
user_data = UserData(photo=obj["assigned_to_photo"], email=obj.get("assigned_to_email") or "")
return get_photo_or_gravatar_url(user_data)

View File

@ -20,6 +20,7 @@ This model contains a domain logic for users application.
from django.apps import apps from django.apps import apps
from django.db.models import Q from django.db.models import Q
from django.db import connection
from django.conf import settings from django.conf import settings
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
@ -142,3 +143,173 @@ def get_stats_for_user(from_user, by_user):
'total_num_closed_userstories': total_num_closed_userstories, 'total_num_closed_userstories': total_num_closed_userstories,
} }
return project_stats return project_stats
def get_voted_content_for_user(user):
"""Returns a dict where:
- The key is the content_type model
- The values are list of id's of the different objects voted by the user
"""
if user.is_anonymous():
return {}
user_votes = {}
for (ct_model, object_id) in user.votes.values_list("content_type__model", "object_id"):
list = user_votes.get(ct_model, [])
list.append(object_id)
user_votes[ct_model] = list
return user_votes
def get_watched_content_for_user(user):
"""Returns a dict where:
- The key is the content_type model
- The values are list of id's of the different objects watched by the user
"""
if user.is_anonymous():
return {}
user_watches = {}
for (ct_model, object_id) in user.watched.values_list("content_type__model", "object_id"):
list = user_watches.get(ct_model, [])
list.append(object_id)
user_watches[ct_model] = list
return user_watches
def _build_favourites_sql_for_type(for_user, type, table_name, ref_column="ref",
project_column="project_id", assigned_to_column="assigned_to_id",
slug_column="slug", subject_column="subject"):
sql = """
SELECT {table_name}.id AS id, {ref_column} AS ref, '{type}' AS type, 'watch' AS action,
tags, notifications_watched.object_id AS object_id, {table_name}.{project_column} AS project,
{slug_column} AS slug, {subject_column} AS subject,
notifications_watched.created_date, coalesce(watchers, 0) as total_watchers, coalesce(votes_votes.count, 0) total_votes, {assigned_to_column} AS assigned_to
FROM notifications_watched
INNER JOIN django_content_type
ON (notifications_watched.content_type_id = django_content_type.id AND django_content_type.model = '{type}')
INNER JOIN {table_name}
ON ({table_name}.id = notifications_watched.object_id)
LEFT JOIN (SELECT object_id, content_type_id, count(*) watchers FROM notifications_watched GROUP BY object_id, content_type_id) type_watchers
ON {table_name}.id = type_watchers.object_id AND django_content_type.id = type_watchers.content_type_id
LEFT JOIN votes_votes
ON ({table_name}.id = votes_votes.object_id AND django_content_type.id = votes_votes.content_type_id)
WHERE notifications_watched.user_id = {for_user_id}
UNION
SELECT {table_name}.id AS id, {ref_column} AS ref, '{type}' AS type, 'vote' AS action,
tags, votes_vote.object_id AS object_id, {table_name}.{project_column} AS project,
{slug_column} AS slug, {subject_column} AS subject,
votes_vote.created_date, coalesce(watchers, 0) as total_watchers, votes_votes.count total_votes, {assigned_to_column} AS assigned_to
FROM votes_vote
INNER JOIN django_content_type
ON (votes_vote.content_type_id = django_content_type.id AND django_content_type.model = '{type}')
INNER JOIN {table_name}
ON ({table_name}.id = votes_vote.object_id)
LEFT JOIN (SELECT object_id, content_type_id, count(*) watchers FROM notifications_watched GROUP BY object_id, content_type_id) type_watchers
ON {table_name}.id = type_watchers.object_id AND django_content_type.id = type_watchers.content_type_id
LEFT JOIN votes_votes
ON ({table_name}.id = votes_votes.object_id AND django_content_type.id = votes_votes.content_type_id)
WHERE votes_vote.user_id = {for_user_id}
"""
sql = sql.format(for_user_id=for_user.id, type=type, table_name=table_name,
ref_column = ref_column, project_column=project_column,
assigned_to_column=assigned_to_column, slug_column=slug_column,
subject_column=subject_column)
return sql
def get_favourites_list(for_user, from_user, type=None, action=None, q=None):
filters_sql = ""
and_needed = False
if type:
filters_sql += " AND type = '{type}' ".format(type=type)
if action:
filters_sql += " AND action = '{action}' ".format(action=action)
if q:
filters_sql += " AND to_tsvector(coalesce(subject, '')) @@ plainto_tsquery('{q}') ".format(q=q)
sql = """
-- BEGIN Basic info: we need to mix info from different tables and denormalize it
SELECT entities.*,
projects_project.name as project_name, projects_project.slug as project_slug, projects_project.is_private as project_is_private,
users_user.username assigned_to_username, users_user.full_name assigned_to_full_name, users_user.photo assigned_to_photo, users_user.email assigned_to_email
FROM (
{userstories_sql}
UNION
{tasks_sql}
UNION
{issues_sql}
UNION
{projects_sql}
) as entities
-- END Basic info
-- BEGIN Project info
LEFT JOIN projects_project
ON (entities.project = projects_project.id)
-- END Project info
-- BEGIN Assigned to user info
LEFT JOIN users_user
ON (assigned_to = users_user.id)
-- END Assigned to user info
-- BEGIN Permissions checking
LEFT JOIN projects_membership
-- Here we check the memberbships from the user requesting the info
ON (projects_membership.user_id = {from_user_id} AND projects_membership.project_id = entities.project)
LEFT JOIN users_role
ON (entities.project = users_role.project_id AND users_role.id = projects_membership.role_id)
WHERE
-- public project
(
projects_project.is_private = false
OR(
-- private project where the view_ permission is included in the user role for that project or in the anon permissions
projects_project.is_private = true
AND(
(entities.type = 'issue' AND 'view_issues' = ANY (array_cat(users_role.permissions, projects_project.anon_permissions)))
OR (entities.type = 'task' AND 'view_tasks' = ANY (array_cat(users_role.permissions, projects_project.anon_permissions)))
OR (entities.type = 'userstory' AND 'view_us' = ANY (array_cat(users_role.permissions, projects_project.anon_permissions)))
OR (entities.type = 'project' AND 'view_project' = ANY (array_cat(users_role.permissions, projects_project.anon_permissions)))
)
))
-- END Permissions checking
{filters_sql}
ORDER BY entities.created_date;
"""
from_user_id = -1
if not from_user.is_anonymous():
from_user_id = from_user.id
sql = sql.format(
for_user_id=for_user.id,
from_user_id=from_user_id,
filters_sql=filters_sql,
userstories_sql=_build_favourites_sql_for_type(for_user, "userstory", "userstories_userstory", slug_column="null"),
tasks_sql=_build_favourites_sql_for_type(for_user, "task", "tasks_task", slug_column="null"),
issues_sql=_build_favourites_sql_for_type(for_user, "issue", "issues_issue", slug_column="null"),
projects_sql=_build_favourites_sql_for_type(for_user, "project", "projects_project",
ref_column="null",
project_column="id",
assigned_to_column="null",
subject_column="projects_project.name")
)
cursor = connection.cursor()
cursor.execute(sql)
desc = cursor.description
return [
dict(zip([col[0] for col in desc], row))
for row in cursor.fetchall()
]

View File

@ -287,3 +287,15 @@ def test_user_action_change_email(client, data):
after_each_request() after_each_request()
results = helper_test_http_method(client, 'post', url, patch_data, users, after_each_request=after_each_request) results = helper_test_http_method(client, 'post', url, patch_data, users, after_each_request=after_each_request)
assert results == [204, 204, 204] assert results == [204, 204, 204]
def test_user_list_votes(client, data):
url = reverse('users-favourites', kwargs={"pk": data.registered_user.pk})
users = [
None,
data.registered_user,
data.other_user,
data.superuser,
]
results = helper_test_http_method(client, 'get', url, None, users)
assert results == [200, 200, 200, 200]

View File

@ -1,6 +1,7 @@
import pytest import pytest
from tempfile import NamedTemporaryFile from tempfile import NamedTemporaryFile
from django.contrib.contenttypes.models import ContentType
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from .. import factories as f from .. import factories as f
@ -9,6 +10,7 @@ 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 from taiga.permissions.permissions import MEMBERS_PERMISSIONS, ANON_PERMISSIONS, USER_PERMISSIONS
from taiga.users.services import get_favourites_list
pytestmark = pytest.mark.django_db pytestmark = pytest.mark.django_db
@ -249,3 +251,166 @@ def test_list_contacts_public_projects(client):
response_content = response.data response_content = response.data
assert len(response_content) == 1 assert len(response_content) == 1
assert response_content[0]["id"] == user_2.id assert response_content[0]["id"] == user_2.id
def test_get_favourites_list():
fav_user = f.UserFactory()
viewer_user = f.UserFactory()
project = f.ProjectFactory(is_private=False, name="Testing project")
role = f.RoleFactory(project=project, permissions=["view_project", "view_us", "view_tasks", "view_issues"])
membership = f.MembershipFactory(project=project, role=role, user=fav_user)
project.add_watcher(fav_user)
content_type = ContentType.objects.get_for_model(project)
f.VoteFactory(content_type=content_type, object_id=project.id, user=fav_user)
f.VotesFactory(content_type=content_type, object_id=project.id, count=1)
user_story = f.UserStoryFactory(project=project, subject="Testing user story")
user_story.add_watcher(fav_user)
content_type = ContentType.objects.get_for_model(user_story)
f.VoteFactory(content_type=content_type, object_id=user_story.id, user=fav_user)
f.VotesFactory(content_type=content_type, object_id=user_story.id, count=1)
task = f.TaskFactory(project=project, subject="Testing task")
task.add_watcher(fav_user)
content_type = ContentType.objects.get_for_model(task)
f.VoteFactory(content_type=content_type, object_id=task.id, user=fav_user)
f.VotesFactory(content_type=content_type, object_id=task.id, count=1)
issue = f.IssueFactory(project=project, subject="Testing issue")
issue.add_watcher(fav_user)
content_type = ContentType.objects.get_for_model(issue)
f.VoteFactory(content_type=content_type, object_id=issue.id, user=fav_user)
f.VotesFactory(content_type=content_type, object_id=issue.id, count=1)
assert len(get_favourites_list(fav_user, viewer_user)) == 8
assert len(get_favourites_list(fav_user, viewer_user, type="project")) == 2
assert len(get_favourites_list(fav_user, viewer_user, type="userstory")) == 2
assert len(get_favourites_list(fav_user, viewer_user, type="task")) == 2
assert len(get_favourites_list(fav_user, viewer_user, type="issue")) == 2
assert len(get_favourites_list(fav_user, viewer_user, type="unknown")) == 0
assert len(get_favourites_list(fav_user, viewer_user, action="watch")) == 4
assert len(get_favourites_list(fav_user, viewer_user, action="vote")) == 4
assert len(get_favourites_list(fav_user, viewer_user, q="issue")) == 2
assert len(get_favourites_list(fav_user, viewer_user, q="unexisting text")) == 0
def test_get_favourites_list_valid_info_for_project():
fav_user = f.UserFactory()
viewer_user = f.UserFactory()
watcher_user = f.UserFactory()
project = f.ProjectFactory(is_private=False, name="Testing project")
project.add_watcher(watcher_user)
content_type = ContentType.objects.get_for_model(project)
vote = f.VoteFactory(content_type=content_type, object_id=project.id, user=fav_user)
f.VotesFactory(content_type=content_type, object_id=project.id, count=1)
project_vote_info = get_favourites_list(fav_user, viewer_user)[0]
assert project_vote_info["type"] == "project"
assert project_vote_info["action"] == "vote"
assert project_vote_info["id"] == project.id
assert project_vote_info["ref"] == None
assert project_vote_info["slug"] == project.slug
assert project_vote_info["subject"] == project.name
assert project_vote_info["tags"] == project.tags
assert project_vote_info["project"] == project.id
assert project_vote_info["assigned_to"] == None
assert project_vote_info["total_watchers"] == 1
assert project_vote_info["created_date"] == vote.created_date
assert project_vote_info["project_name"] == project.name
assert project_vote_info["project_slug"] == project.slug
assert project_vote_info["project_is_private"] == project.is_private
assert project_vote_info["assigned_to_username"] == None
assert project_vote_info["assigned_to_full_name"] == None
assert project_vote_info["assigned_to_photo"] == None
assert project_vote_info["assigned_to_email"] == None
assert project_vote_info["total_votes"] == 1
def test_get_favourites_list_valid_info_for_not_project_types():
fav_user = f.UserFactory()
viewer_user = f.UserFactory()
watcher_user = f.UserFactory()
assigned_to_user = f.UserFactory()
project = f.ProjectFactory(is_private=False, name="Testing project")
factories = {
"userstory": f.UserStoryFactory,
"task": f.TaskFactory,
"issue": f.IssueFactory
}
for object_type in factories:
instance = factories[object_type](project=project,
subject="Testing",
tags=["test1", "test2"],
assigned_to=assigned_to_user)
instance.add_watcher(watcher_user)
content_type = ContentType.objects.get_for_model(instance)
vote = f.VoteFactory(content_type=content_type, object_id=instance.id, user=fav_user)
f.VotesFactory(content_type=content_type, object_id=instance.id, count=3)
instance_vote_info = get_favourites_list(fav_user, viewer_user, type=object_type)[0]
assert instance_vote_info["type"] == object_type
assert instance_vote_info["action"] == "vote"
assert instance_vote_info["id"] == instance.id
assert instance_vote_info["ref"] == instance.ref
assert instance_vote_info["slug"] == None
assert instance_vote_info["subject"] == instance.subject
assert instance_vote_info["tags"] == instance.tags
assert instance_vote_info["project"] == instance.project.id
assert instance_vote_info["assigned_to"] == assigned_to_user.id
assert instance_vote_info["total_watchers"] == 1
assert instance_vote_info["created_date"] == vote.created_date
assert instance_vote_info["project_name"] == instance.project.name
assert instance_vote_info["project_slug"] == instance.project.slug
assert instance_vote_info["project_is_private"] == instance.project.is_private
assert instance_vote_info["assigned_to_username"] == assigned_to_user.username
assert instance_vote_info["assigned_to_full_name"] == assigned_to_user.full_name
assert instance_vote_info["assigned_to_photo"] == ''
assert instance_vote_info["assigned_to_email"] == assigned_to_user.email
assert instance_vote_info["total_votes"] == 3
def test_get_favourites_list_permissions():
fav_user = f.UserFactory()
viewer_unpriviliged_user = f.UserFactory()
viewer_priviliged_user = f.UserFactory()
project = f.ProjectFactory(is_private=True, name="Testing project")
role = f.RoleFactory(project=project, permissions=["view_project", "view_us", "view_tasks", "view_issues"])
membership = f.MembershipFactory(project=project, role=role, user=viewer_priviliged_user)
content_type = ContentType.objects.get_for_model(project)
f.VoteFactory(content_type=content_type, object_id=project.id, user=fav_user)
f.VotesFactory(content_type=content_type, object_id=project.id, count=1)
user_story = f.UserStoryFactory(project=project, subject="Testing user story")
content_type = ContentType.objects.get_for_model(user_story)
f.VoteFactory(content_type=content_type, object_id=user_story.id, user=fav_user)
f.VotesFactory(content_type=content_type, object_id=user_story.id, count=1)
task = f.TaskFactory(project=project, subject="Testing task")
content_type = ContentType.objects.get_for_model(task)
f.VoteFactory(content_type=content_type, object_id=task.id, user=fav_user)
f.VotesFactory(content_type=content_type, object_id=task.id, count=1)
issue = f.IssueFactory(project=project, subject="Testing issue")
content_type = ContentType.objects.get_for_model(issue)
f.VoteFactory(content_type=content_type, object_id=issue.id, user=fav_user)
f.VotesFactory(content_type=content_type, object_id=issue.id, count=1)
#If the project is private a viewer user without any permission shouldn' see any vote
assert len(get_favourites_list(fav_user, viewer_unpriviliged_user)) == 0
#If the project is private but the viewer user has permissions the votes should be accesible
assert len(get_favourites_list(fav_user, viewer_priviliged_user)) == 4
#If the project is private but has the required anon permissions the votes should be accesible by any user too
project.anon_permissions = ["view_project", "view_us", "view_tasks", "view_issues"]
project.save()
assert len(get_favourites_list(fav_user, viewer_unpriviliged_user)) == 4