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)
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"])
def password_recovery(self, request, pk=None):
username_or_email = request.DATA.get('username', None)

View File

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

View File

@ -19,11 +19,14 @@ from django.core.exceptions import ValidationError
from django.utils.translation import ugettext_lazy as _
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 .models import User, Role
from .services import get_photo_or_gravatar_url, get_big_photo_or_gravatar_url
from collections import namedtuple
import re
@ -149,3 +152,55 @@ class ProjectRoleSerializer(serializers.ModelSerializer):
model = Role
fields = ('id', 'name', 'slug', 'order', 'computable')
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.db.models import Q
from django.db import connection
from django.conf import settings
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,
}
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()
results = helper_test_http_method(client, 'post', url, patch_data, users, after_each_request=after_each_request)
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
from tempfile import NamedTemporaryFile
from django.contrib.contenttypes.models import ContentType
from django.core.urlresolvers import reverse
from .. import factories as f
@ -9,6 +10,7 @@ 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
from taiga.users.services import get_favourites_list
pytestmark = pytest.mark.django_db
@ -249,3 +251,166 @@ def test_list_contacts_public_projects(client):
response_content = response.data
assert len(response_content) == 1
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