Fix some errors related to watched and likes lists
parent
0d21f04a87
commit
bf57ace9a2
|
@ -189,14 +189,15 @@ 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))
|
||||
#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
|
||||
|
||||
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))
|
||||
|
||||
|
@ -204,14 +205,14 @@ class WatchedResourceModelSerializer(serializers.ModelSerializer):
|
|||
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)
|
||||
services.add_watcher(obj, user)
|
||||
|
||||
for user in removing_users:
|
||||
services.remove_watcher(instance, user)
|
||||
services.remove_watcher(obj, user)
|
||||
|
||||
instance.watchers = instance.get_watchers()
|
||||
obj.watchers = obj.get_watchers()
|
||||
|
||||
return instance
|
||||
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
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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():
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue