From bf57ace9a2a5229ace08ce034f6ab618eb85273c Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Wed, 9 Sep 2015 15:32:18 +0200 Subject: [PATCH] Fix some errors related to watched and likes lists --- taiga/projects/notifications/mixins.py | 37 +++++++------- taiga/projects/notifications/services.py | 17 +++++++ taiga/users/serializers.py | 62 ++++++++++++++++++---- taiga/users/services.py | 45 +++++++++++----- tests/integration/test_users.py | 65 ++++++++++++++++-------- tests/integration/test_userstories.py | 16 ++++++ 6 files changed, 181 insertions(+), 61 deletions(-) diff --git a/taiga/projects/notifications/mixins.py b/taiga/projects/notifications/mixins.py index ee41fd55..27a93d88 100644 --- a/taiga/projects/notifications/mixins.py +++ b/taiga/projects/notifications/mixins.py @@ -189,29 +189,30 @@ class WatchedResourceModelSerializer(serializers.ModelSerializer): #watchers is not a field from the model but can be attached in the get_queryset of the viewset. #If that's the case we need to remove it before calling the super method watcher_field = self.fields.pop("watchers", None) - instance = super(WatchedResourceModelSerializer, self).restore_object(attrs, instance) - if instance is not None and self.validate_watchers(attrs, "watchers"): - #A partial update can exclude the watchers field - if not "watchers" in attrs: - return instance + self.validate_watchers(attrs, "watchers") + new_watcher_ids = set(attrs.pop("watchers", [])) + obj = super(WatchedResourceModelSerializer, self).restore_object(attrs, instance) - new_watcher_ids = set(attrs.get("watchers", None)) - old_watcher_ids = set(instance.get_watchers().values_list("id", flat=True)) - adding_watcher_ids = list(new_watcher_ids.difference(old_watcher_ids)) - removing_watcher_ids = list(old_watcher_ids.difference(new_watcher_ids)) + #A partial update can exclude the watchers field or if the new instance can still not be saved + if instance is None or len(new_watcher_ids) == 0: + return obj - User = apps.get_model("users", "User") - adding_users = User.objects.filter(id__in=adding_watcher_ids) - removing_users = User.objects.filter(id__in=removing_watcher_ids) - for user in adding_users: - services.add_watcher(instance, user) + old_watcher_ids = set(obj.get_watchers().values_list("id", flat=True)) + adding_watcher_ids = list(new_watcher_ids.difference(old_watcher_ids)) + removing_watcher_ids = list(old_watcher_ids.difference(new_watcher_ids)) - for user in removing_users: - services.remove_watcher(instance, user) + User = apps.get_model("users", "User") + adding_users = User.objects.filter(id__in=adding_watcher_ids) + removing_users = User.objects.filter(id__in=removing_watcher_ids) + for user in adding_users: + services.add_watcher(obj, user) - instance.watchers = instance.get_watchers() + for user in removing_users: + services.remove_watcher(obj, user) - return instance + obj.watchers = obj.get_watchers() + + return obj def to_native(self, obj): #watchers is wasn't attached via the get_queryset of the viewset we need to manually add it diff --git a/taiga/projects/notifications/services.py b/taiga/projects/notifications/services.py index 1cc99dc6..e4bd8a3f 100644 --- a/taiga/projects/notifications/services.py +++ b/taiga/projects/notifications/services.py @@ -367,6 +367,23 @@ def get_watched(user_or_id, model): params=(obj_type.id, user_id)) +def get_projects_watched(user_or_id): + """Get the objects watched by an user. + + :param user_or_id: :class:`~taiga.users.models.User` instance or id. + :param model: Show only objects of this kind. Can be any Django model class. + + :return: Queryset of objects representing the votes of the user. + """ + + if isinstance(user_or_id, get_user_model()): + user_id = user_or_id.id + else: + user_id = user_or_id + + project_class = apps.get_model("projects", "Project") + return project_class.objects.filter(notify_policies__user__id=user_id).exclude(notify_policies__notify_level=NotifyLevel.ignore) + def add_watcher(obj, user): """Add a watcher to an object. diff --git a/taiga/users/serializers.py b/taiga/users/serializers.py index 3158f5f8..2a9abade 100644 --- a/taiga/users/serializers.py +++ b/taiga/users/serializers.py @@ -165,27 +165,31 @@ class FavouriteSerializer(serializers.Serializer): id = serializers.IntegerField() ref = serializers.IntegerField() slug = serializers.CharField() + name = serializers.CharField() subject = serializers.CharField() - tags = TagsField(default=[]) - project = serializers.IntegerField() + description = serializers.SerializerMethodField("get_description") assigned_to = serializers.IntegerField() - total_watchers = serializers.IntegerField() + status = serializers.CharField() + status_color = serializers.CharField() + tags_colors = serializers.SerializerMethodField("get_tags_color") + created_date = serializers.DateTimeField() + is_private = serializers.SerializerMethodField("get_is_private") is_voted = serializers.SerializerMethodField("get_is_voted") is_watched = serializers.SerializerMethodField("get_is_watched") - created_date = serializers.DateTimeField() + total_watchers = serializers.IntegerField() + total_votes = serializers.IntegerField() - project_name = serializers.CharField() - project_slug = serializers.CharField() - project_is_private = serializers.CharField() + project = serializers.SerializerMethodField("get_project") + project_name = serializers.SerializerMethodField("get_project_name") + project_slug = serializers.SerializerMethodField("get_project_slug") + project_is_private = serializers.SerializerMethodField("get_project_is_private") 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", {}) @@ -194,6 +198,38 @@ class FavouriteSerializer(serializers.Serializer): # Instantiate the superclass normally super(FavouriteSerializer, self).__init__(*args, **kwargs) + def _none_if_project(self, obj, property): + type = obj.get("type", "") + if type == "project": + return None + + return obj.get(property) + + def _none_if_not_project(self, obj, property): + type = obj.get("type", "") + if type != "project": + return None + + return obj.get(property) + + def get_project(self, obj): + return self._none_if_project(obj, "project") + + def get_is_private(self, obj): + return self._none_if_not_project(obj, "project_is_private") + + def get_project_name(self, obj): + return self._none_if_project(obj, "project_name") + + def get_description(self, obj): + return self._none_if_not_project(obj, "description") + + def get_project_slug(self, obj): + return self._none_if_project(obj, "project_slug") + + def get_project_is_private(self, obj): + return self._none_if_project(obj, "project_is_private") + def get_is_voted(self, obj): return obj["id"] in self.user_votes.get(obj["type"], []) @@ -201,6 +237,14 @@ class FavouriteSerializer(serializers.Serializer): return obj["id"] in self.user_watching.get(obj["type"], []) def get_photo(self, obj): + type = obj.get("type", "") + if type == "project": + return None + 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) + + def get_tags_color(self, obj): + tags = obj.get("tags", []) + return [{"name": tc[0], "color": tc[1]} for tc in obj.get("tags_colors", []) if tc[0] in tags] diff --git a/taiga/users/services.py b/taiga/users/services.py index 4be2f405..580d615b 100644 --- a/taiga/users/services.py +++ b/taiga/users/services.py @@ -31,7 +31,7 @@ from easy_thumbnails.exceptions import InvalidImageFormatError from taiga.base import exceptions as exc from taiga.base.utils.urls import get_absolute_url from taiga.projects.notifications.choices import NotifyLevel - +from taiga.projects.notifications.services import get_projects_watched from .gravatar import get_gravatar_url from django.conf import settings @@ -179,6 +179,11 @@ def get_watched_content_for_user(user): list.append(object_id) user_watches[ct_model] = list + #Now for projects, + projects_watched = get_projects_watched(user) + project_content_type_model=ContentType.objects.get(app_label="projects", model="project").model + user_watches[project_content_type_model] = projects_watched.values_list("id", flat=True) + return user_watches @@ -186,8 +191,9 @@ def _build_favourites_sql_for_projects(for_user): sql = """ SELECT projects_project.id AS id, null AS ref, 'project' AS type, 'watch' AS action, tags, notifications_notifypolicy.project_id AS object_id, projects_project.id AS project, - slug AS slug, projects_project.name AS subject, - notifications_notifypolicy.created_at, coalesce(watchers, 0) as total_watchers, coalesce(votes_votes.count, 0) total_votes, null AS assigned_to + slug AS slug, projects_project.name AS name, null AS subject, + notifications_notifypolicy.created_at as created_date, coalesce(watchers, 0) as total_watchers, coalesce(votes_votes.count, 0) total_votes, null AS assigned_to, + null as status, null as status_color FROM notifications_notifypolicy INNER JOIN projects_project ON (projects_project.id = notifications_notifypolicy.project_id) @@ -203,8 +209,9 @@ def _build_favourites_sql_for_projects(for_user): UNION SELECT projects_project.id AS id, null AS ref, 'project' AS type, 'vote' AS action, tags, votes_vote.object_id AS object_id, projects_project.id AS project, - slug AS slug, projects_project.name AS subject, - votes_vote.created_date, coalesce(watchers, 0) as total_watchers, coalesce(votes_votes.count, 0) total_votes, null AS assigned_to + slug AS slug, projects_project.name AS name, null AS subject, + votes_vote.created_date, coalesce(watchers, 0) as total_watchers, coalesce(votes_votes.count, 0) total_votes, null AS assigned_to, + null as status, null as status_color FROM votes_vote INNER JOIN projects_project ON (projects_project.id = votes_vote.object_id) @@ -216,7 +223,7 @@ def _build_favourites_sql_for_projects(for_user): ON projects_project.id = type_watchers.project_id LEFT JOIN votes_votes ON (projects_project.id = votes_votes.object_id AND {project_content_type_id} = votes_votes.content_type_id) - WHERE votes_vote.user_id = {for_user_id} + WHERE votes_vote.user_id = {for_user_id} AND {project_content_type_id} = votes_vote.content_type_id """ sql = sql.format( for_user_id=for_user.id, @@ -232,13 +239,16 @@ def _build_favourites_sql_for_type(for_user, type, table_name, ref_column="ref", 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 + {slug_column} AS slug, null AS name, {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, + projects_{type}status.name as status, projects_{type}status.color as status_color 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) + INNER JOIN projects_{type}status + ON (projects_{type}status.id = {table_name}.status_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 @@ -247,13 +257,16 @@ def _build_favourites_sql_for_type(for_user, type, table_name, ref_column="ref", 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 + {slug_column} AS slug, null AS name, {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, + projects_{type}status.name as status, projects_{type}status.color as status_color 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) + INNER JOIN projects_{type}status + ON (projects_{type}status.id = {table_name}.status_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 @@ -279,12 +292,18 @@ def get_favourites_list(for_user, from_user, type=None, action=None, q=None): filters_sql += " AND action = '{action}' ".format(action=action) if q: - filters_sql += " AND to_tsvector(coalesce(subject, '')) @@ plainto_tsquery('{q}') ".format(q=q) + # We must transform a q like "proj exam" (should find "project example") to something like proj:* & exam:* + qs = ["{}:*".format(e) for e in q.split(" ")] + filters_sql += """ AND ( + to_tsvector(coalesce(subject,'') || ' ' ||coalesce(entities.name,'') || ' ' ||coalesce(to_char(ref, '999'),'')) @@ to_tsquery('{q}') + ) + """.format(q=" & ".join(qs)) 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, + projects_project.name as project_name, projects_project.description as description, projects_project.slug as project_slug, projects_project.is_private as project_is_private, + projects_project.tags_colors, 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} @@ -332,7 +351,7 @@ def get_favourites_list(for_user, from_user, type=None, action=None, q=None): -- END Permissions checking {filters_sql} - ORDER BY entities.created_date; + ORDER BY entities.created_date DESC; """ from_user_id = -1 diff --git a/tests/integration/test_users.py b/tests/integration/test_users.py index f20049f1..4ad11470 100644 --- a/tests/integration/test_users.py +++ b/tests/integration/test_users.py @@ -9,6 +9,7 @@ from .. import factories as f from taiga.base.utils import json from taiga.users import models +from taiga.users.serializers import FavouriteSerializer 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 @@ -396,32 +397,43 @@ def test_get_favourites_list_valid_info_for_project(): viewer_user = f.UserFactory() watcher_user = f.UserFactory() - project = f.ProjectFactory(is_private=False, name="Testing project") + project = f.ProjectFactory(is_private=False, name="Testing project", tags=['test', 'tag']) 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] + raw_project_vote_info = get_favourites_list(fav_user, viewer_user)[0] + project_vote_info = FavouriteSerializer(raw_project_vote_info).data + 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["name"] == project.name + assert project_vote_info["subject"] == None + assert project_vote_info["description"] == project.description assert project_vote_info["assigned_to"] == None + assert project_vote_info["status"] == None + assert project_vote_info["status_color"] == None + + tags_colors = {tc["name"]:tc["color"] for tc in project_vote_info["tags_colors"]} + assert "test" in tags_colors + assert "tag" in tags_colors + + assert project_vote_info["is_private"] == project.is_private + assert project_vote_info["is_voted"] == False + assert project_vote_info["is_watched"] == False 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["total_votes"] == 1 + assert project_vote_info["project"] == None + assert project_vote_info["project_name"] == None + assert project_vote_info["project_slug"] == None + assert project_vote_info["project_is_private"] == None 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(): @@ -449,26 +461,37 @@ def test_get_favourites_list_valid_info_for_not_project_types(): 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] + raw_instance_vote_info = get_favourites_list(fav_user, viewer_user, type=object_type)[0] + instance_vote_info = FavouriteSerializer(raw_instance_vote_info).data + 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["name"] == 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["description"] == None + assert instance_vote_info["assigned_to"] == instance.assigned_to.id + assert instance_vote_info["status"] == instance.status.name + assert instance_vote_info["status_color"] == instance.status.color + + tags_colors = {tc["name"]:tc["color"] for tc in instance_vote_info["tags_colors"]} + assert "test1" in tags_colors + assert "test2" in tags_colors + + assert instance_vote_info["is_private"] == None + assert instance_vote_info["is_voted"] == False + assert instance_vote_info["is_watched"] == False assert instance_vote_info["total_watchers"] == 1 - assert instance_vote_info["created_date"] == vote.created_date + assert instance_vote_info["total_votes"] == 3 + assert instance_vote_info["project"] == instance.project.id 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 + assert instance_vote_info["assigned_to_username"] == instance.assigned_to.username + assert instance_vote_info["assigned_to_full_name"] == instance.assigned_to.full_name + assert instance_vote_info["assigned_to_photo"] != "" def test_get_favourites_list_permissions(): diff --git a/tests/integration/test_userstories.py b/tests/integration/test_userstories.py index f90fd97a..787656e8 100644 --- a/tests/integration/test_userstories.py +++ b/tests/integration/test_userstories.py @@ -46,6 +46,22 @@ def test_update_userstories_order_in_bulk(): model=models.UserStory) +def test_create_userstory_with_watchers(client): + user = f.UserFactory.create() + user_watcher = f.UserFactory.create() + project = f.ProjectFactory.create(owner=user) + f.MembershipFactory.create(project=project, user=user, is_owner=True) + f.MembershipFactory.create(project=project, user=user_watcher, is_owner=True) + url = reverse("userstories-list") + + data = {"subject": "Test user story", "project": project.id, "watchers": [user_watcher.id]} + client.login(user) + response = client.json.post(url, json.dumps(data)) + print(response.data) + assert response.status_code == 201 + assert response.data["watchers"] == [] + + def test_create_userstory_without_status(client): user = f.UserFactory.create() project = f.ProjectFactory.create(owner=user)