Merge pull request #392 from taigaio/issue/3019/duplicate-entries-in-profile-timelines

Issue/3019/duplicate entries in profile timelines and more
remotes/origin/enhancement/email-actions
Alejandro 2015-07-23 12:49:57 +02:00
commit d5c800d3e5
7 changed files with 115 additions and 38 deletions

View File

@ -10,6 +10,7 @@
- Fix the compatibility with BitBucket webhooks and add issues and issues comments integration. - Fix the compatibility with BitBucket webhooks and add issues and issues comments integration.
- Add custom videoconference system. - Add custom videoconference system.
- Add support for comments in the Gitlab webhooks integration. - Add support for comments in the Gitlab webhooks integration.
- Now profile timelines only show content about the objects (US/Tasks/Issues/Wiki pages) you are involved.
### Misc ### Misc
- API: Mixin fields 'users', 'members' and 'memberships' in ProjectDetailSerializer - API: Mixin fields 'users', 'members' and 'memberships' in ProjectDetailSerializer

View File

@ -0,0 +1,35 @@
# 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.core.management.base import BaseCommand
from django.test.utils import override_settings
from django.core.management import call_command
from taiga.projects.models import Project
class Command(BaseCommand):
help = 'Regenerate projects timeline iterating per project'
@override_settings(DEBUG=False)
def handle(self, *args, **options):
total = Project.objects.count()
for count,project in enumerate(Project.objects.order_by("id")):
print("""***********************************
%s/%s %s
***********************************"""%(count+1, total, project.name))
call_command("rebuild_timeline", project=project.id)

View File

@ -39,38 +39,44 @@ def _push_to_timeline(*args, **kwargs):
def _push_to_timelines(project, user, obj, event_type, created_datetime, extra_data={}): def _push_to_timelines(project, user, obj, event_type, created_datetime, extra_data={}):
if project is not None: if project is not None:
# Project timeline # Actions related with a project
## Project timeline
_push_to_timeline(project, obj, event_type, created_datetime, _push_to_timeline(project, obj, event_type, created_datetime,
namespace=build_project_namespace(project), namespace=build_project_namespace(project),
extra_data=extra_data) extra_data=extra_data)
# User timeline ## User profile timelines
_push_to_timeline(user, obj, event_type, created_datetime, ## - Me
namespace=build_user_namespace(user), related_people = User.objects.filter(id=user.id)
extra_data=extra_data)
# Calculating related people ## - Owner
related_people = User.objects.none() if hasattr(obj, "owner_id") and obj.owner_id:
related_people |= User.objects.filter(id=obj.owner_id)
# Assigned to ## - Assigned to
if hasattr(obj, "assigned_to") and obj.assigned_to and user != obj.assigned_to: if hasattr(obj, "assigned_to_id") and obj.assigned_to_id:
related_people |= User.objects.filter(id=obj.assigned_to.id) related_people |= User.objects.filter(id=obj.assigned_to_id)
# Watchers ## - Watchers
watchers = hasattr(obj, "watchers") and obj.watchers.exclude(id=user.id) or User.objects.none() watchers = getattr(obj, "watchers", None)
if watchers: if watchers:
related_people |= watchers related_people |= obj.watchers.all()
if project is not None: ## - Exclude inactive and system users and remove duplicate
# Team related_people = related_people.exclude(is_active=False)
team_members_ids = project.memberships.filter(user__isnull=False).values_list("id", flat=True) related_people = related_people.exclude(is_system=True)
team = User.objects.filter(id__in=team_members_ids)
related_people |= team
related_people = related_people.distinct() related_people = related_people.distinct()
_push_to_timeline(related_people, obj, event_type, created_datetime, _push_to_timeline(related_people, obj, event_type, created_datetime,
namespace=build_user_namespace(user), namespace=build_user_namespace(user),
extra_data=extra_data) extra_data=extra_data)
else:
# Actions not related with a project
## - Me
_push_to_timeline(user, obj, event_type, created_datetime,
namespace=build_user_namespace(user),
extra_data=extra_data)
def on_new_history_entry(sender, instance, created, **kwargs): def on_new_history_entry(sender, instance, created, **kwargs):
@ -110,15 +116,22 @@ def on_new_history_entry(sender, instance, created, **kwargs):
def create_membership_push_to_timeline(sender, instance, **kwargs): def create_membership_push_to_timeline(sender, instance, **kwargs):
# Creating new membership with associated user """
# If the user is the project owner we don't do anything because that info will Creating new membership with associated user. If the user is the project owner we don't
# be shown in created project timeline entry do anything because that info will be shown in created project timeline entry
@param sender: Membership model
@param instance: Membership object
"""
# We shown in created project timeline entry
if not instance.pk and instance.user and instance.user != instance.project.owner: if not instance.pk and instance.user and instance.user != instance.project.owner:
created_datetime = instance.created_at created_datetime = instance.created_at
_push_to_timelines(instance.project, instance.user, instance, "create", created_datetime) _push_to_timelines(instance.project, instance.user, instance, "create", created_datetime)
#Updating existing membership # Updating existing membership
elif instance.pk: elif instance.pk:
try:
prev_instance = sender.objects.get(pk=instance.pk) prev_instance = sender.objects.get(pk=instance.pk)
if instance.user != prev_instance.user: if instance.user != prev_instance.user:
created_datetime = timezone.now() created_datetime = timezone.now()
@ -131,6 +144,9 @@ def create_membership_push_to_timeline(sender, instance, **kwargs):
prev_instance, prev_instance,
"delete", "delete",
created_datetime) created_datetime)
except sender.DoesNotExist:
# This happens with some tests, when a membership is created with a concrete id
pass
def delete_membership_push_to_timeline(sender, instance, **kwargs): def delete_membership_push_to_timeline(sender, instance, **kwargs):

View File

@ -377,7 +377,7 @@ def test_owner_user_story_timeline():
def test_assigned_to_user_story_timeline(): def test_assigned_to_user_story_timeline():
membership = factories.MembershipFactory.create() membership = factories.MembershipFactory.create()
user_story = factories.UserStoryFactory.create(subject="test us timeline", assigned_to=membership.user) user_story = factories.UserStoryFactory.create(subject="test us timeline", assigned_to=membership.user, project=membership.project)
history_services.take_snapshot(user_story, user=user_story.owner) history_services.take_snapshot(user_story, user=user_story.owner)
user_timeline = service.get_profile_timeline(user_story.assigned_to) user_timeline = service.get_profile_timeline(user_story.assigned_to)
assert user_timeline[0].event_type == "userstories.userstory.create" assert user_timeline[0].event_type == "userstories.userstory.create"
@ -386,20 +386,22 @@ def test_assigned_to_user_story_timeline():
def test_watchers_to_user_story_timeline(): def test_watchers_to_user_story_timeline():
membership = factories.MembershipFactory.create() membership = factories.MembershipFactory.create()
user_story = factories.UserStoryFactory.create(subject="test us timeline") user_story = factories.UserStoryFactory.create(subject="test us timeline", project=membership.project)
user_story.watchers.add(membership.user) user_story.watchers.add(membership.user)
history_services.take_snapshot(user_story, user=user_story.owner) history_services.take_snapshot(user_story, user=user_story.owner)
user_timeline = service.get_profile_timeline(membership.user) user_timeline = service.get_profile_timeline(membership.user)
assert user_timeline[0].event_type == "userstories.userstory.create" assert user_timeline[0].event_type == "userstories.userstory.create"
assert user_timeline[0].data["userstory"]["subject"] == "test us timeline" assert user_timeline[0].data["userstory"]["subject"] == "test us timeline"
def test_user_data_for_system_users():
def test_user_data_for_non_system_users():
user_story = factories.UserStoryFactory.create(subject="test us timeline") user_story = factories.UserStoryFactory.create(subject="test us timeline")
history_services.take_snapshot(user_story, user=user_story.owner) history_services.take_snapshot(user_story, user=user_story.owner)
project_timeline = service.get_project_timeline(user_story.project) project_timeline = service.get_project_timeline(user_story.project)
serialized_obj = TimelineSerializer(project_timeline[0]) serialized_obj = TimelineSerializer(project_timeline[0])
serialized_obj.data["data"]["user"]["is_profile_visible"] = True serialized_obj.data["data"]["user"]["is_profile_visible"] = True
def test_user_data_for_system_users(): def test_user_data_for_system_users():
user_story = factories.UserStoryFactory.create(subject="test us timeline") user_story = factories.UserStoryFactory.create(subject="test us timeline")
user_story.owner.is_system = True user_story.owner.is_system = True
@ -409,6 +411,7 @@ def test_user_data_for_system_users():
serialized_obj = TimelineSerializer(project_timeline[0]) serialized_obj = TimelineSerializer(project_timeline[0])
serialized_obj.data["data"]["user"]["is_profile_visible"] = False serialized_obj.data["data"]["user"]["is_profile_visible"] = False
def test_user_data_for_unactived_users(): def test_user_data_for_unactived_users():
user_story = factories.UserStoryFactory.create(subject="test us timeline") user_story = factories.UserStoryFactory.create(subject="test us timeline")
user_story.owner.cancel() user_story.owner.cancel()
@ -418,3 +421,25 @@ def test_user_data_for_unactived_users():
serialized_obj = TimelineSerializer(project_timeline[0]) serialized_obj = TimelineSerializer(project_timeline[0])
serialized_obj.data["data"]["user"]["is_profile_visible"] = False serialized_obj.data["data"]["user"]["is_profile_visible"] = False
serialized_obj.data["data"]["user"]["username"] = "deleted-user" serialized_obj.data["data"]["user"]["username"] = "deleted-user"
def test_timeline_error_use_member_ids_instead_of_memberships_ids():
user_story = factories.UserStoryFactory.create(subject="test error use member ids instead of "
"memberships ids")
member_user = user_story.owner
external_user = factories.UserFactory.create()
membership = factories.MembershipFactory.create(project=user_story.project,
user=member_user,
id=external_user.id)
history_services.take_snapshot(user_story, user=member_user)
user_timeline = service.get_profile_timeline(member_user)
assert len(user_timeline) == 2
assert user_timeline[0].event_type == "userstories.userstory.create"
assert user_timeline[1].event_type == "users.user.create"
external_user_timeline = service.get_profile_timeline(external_user)
assert len(external_user_timeline) == 1
assert external_user_timeline[0].event_type == "users.user.create"