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.
- Add custom videoconference system.
- 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
- 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={}):
if project is not None:
# Project timeline
# Actions related with a project
## Project timeline
_push_to_timeline(project, obj, event_type, created_datetime,
namespace=build_project_namespace(project),
extra_data=extra_data)
# User timeline
_push_to_timeline(user, obj, event_type, created_datetime,
namespace=build_user_namespace(user),
extra_data=extra_data)
## User profile timelines
## - Me
related_people = User.objects.filter(id=user.id)
# Calculating related people
related_people = User.objects.none()
## - Owner
if hasattr(obj, "owner_id") and obj.owner_id:
related_people |= User.objects.filter(id=obj.owner_id)
# Assigned to
if hasattr(obj, "assigned_to") and obj.assigned_to and user != obj.assigned_to:
related_people |= User.objects.filter(id=obj.assigned_to.id)
## - Assigned to
if hasattr(obj, "assigned_to_id") and obj.assigned_to_id:
related_people |= User.objects.filter(id=obj.assigned_to_id)
# Watchers
watchers = hasattr(obj, "watchers") and obj.watchers.exclude(id=user.id) or User.objects.none()
if watchers:
related_people |= watchers
## - Watchers
watchers = getattr(obj, "watchers", None)
if watchers:
related_people |= obj.watchers.all()
if project is not None:
# Team
team_members_ids = project.memberships.filter(user__isnull=False).values_list("id", flat=True)
team = User.objects.filter(id__in=team_members_ids)
related_people |= team
## - Exclude inactive and system users and remove duplicate
related_people = related_people.exclude(is_active=False)
related_people = related_people.exclude(is_system=True)
related_people = related_people.distinct()
_push_to_timeline(related_people, obj, event_type, created_datetime,
namespace=build_user_namespace(user),
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):
@ -110,27 +116,37 @@ def on_new_history_entry(sender, instance, created, **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
# be shown in created project timeline entry
"""
Creating new membership with associated user. If the user is the project owner we don't
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:
created_datetime = instance.created_at
_push_to_timelines(instance.project, instance.user, instance, "create", created_datetime)
#Updating existing membership
# Updating existing membership
elif instance.pk:
prev_instance = sender.objects.get(pk=instance.pk)
if instance.user != prev_instance.user:
created_datetime = timezone.now()
# The new member
_push_to_timelines(instance.project, instance.user, instance, "create", created_datetime)
# If we are updating the old user is removed from project
if prev_instance.user:
_push_to_timelines(instance.project,
prev_instance.user,
prev_instance,
"delete",
created_datetime)
try:
prev_instance = sender.objects.get(pk=instance.pk)
if instance.user != prev_instance.user:
created_datetime = timezone.now()
# The new member
_push_to_timelines(instance.project, instance.user, instance, "create", created_datetime)
# If we are updating the old user is removed from project
if prev_instance.user:
_push_to_timelines(instance.project,
prev_instance.user,
prev_instance,
"delete",
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):

View File

@ -377,7 +377,7 @@ def test_owner_user_story_timeline():
def test_assigned_to_user_story_timeline():
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)
user_timeline = service.get_profile_timeline(user_story.assigned_to)
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():
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)
history_services.take_snapshot(user_story, user=user_story.owner)
user_timeline = service.get_profile_timeline(membership.user)
assert user_timeline[0].event_type == "userstories.userstory.create"
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")
history_services.take_snapshot(user_story, user=user_story.owner)
project_timeline = service.get_project_timeline(user_story.project)
serialized_obj = TimelineSerializer(project_timeline[0])
serialized_obj.data["data"]["user"]["is_profile_visible"] = True
def test_user_data_for_system_users():
user_story = factories.UserStoryFactory.create(subject="test us timeline")
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.data["data"]["user"]["is_profile_visible"] = False
def test_user_data_for_unactived_users():
user_story = factories.UserStoryFactory.create(subject="test us timeline")
user_story.owner.cancel()
@ -418,3 +421,25 @@ def test_user_data_for_unactived_users():
serialized_obj = TimelineSerializer(project_timeline[0])
serialized_obj.data["data"]["user"]["is_profile_visible"] = False
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"