diff --git a/taiga/timeline/apps.py b/taiga/timeline/apps.py index f7790905..cbdea045 100644 --- a/taiga/timeline/apps.py +++ b/taiga/timeline/apps.py @@ -32,3 +32,5 @@ class TimelineAppConfig(AppConfig): sender=apps.get_model("projects", "Membership")) signals.post_delete.connect(handlers.delete_membership_push_to_timeline, sender=apps.get_model("projects", "Membership")) + signals.post_save.connect(handlers.create_user_push_to_timeline, + sender=apps.get_model("users", "User")) diff --git a/taiga/timeline/management/commands/rebuild_timeline_for_user_creation.py b/taiga/timeline/management/commands/rebuild_timeline_for_user_creation.py new file mode 100644 index 00000000..d4f99ed5 --- /dev/null +++ b/taiga/timeline/management/commands/rebuild_timeline_for_user_creation.py @@ -0,0 +1,98 @@ +# Copyright (C) 2014 Andrey Antukh +# Copyright (C) 2014 Jesús Espino +# Copyright (C) 2014 David Barragán +# 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 . + +# Examples: +# python manage.py rebuild_timeline_for_user_creation --settings=settings.local_timeline + +from django.conf import settings +from django.contrib.contenttypes.models import ContentType +from django.core.exceptions import ObjectDoesNotExist +from django.core.management.base import BaseCommand +from django.db.models import Model +from django.db import reset_queries + +from taiga.timeline.service import (_get_impl_key_from_model, + _timeline_impl_map, extract_user_info) +from taiga.timeline.models import Timeline +from taiga.timeline.signals import _push_to_timelines +from taiga.users.models import User + +from unittest.mock import patch + +import gc + +class BulkCreator(object): + def __init__(self): + self.timeline_objects = [] + self.created = None + + def create_element(self, element): + self.timeline_objects.append(element) + if len(self.timeline_objects) > 1000: + self.flush() + + def flush(self): + Timeline.objects.bulk_create(self.timeline_objects, batch_size=1000) + del self.timeline_objects + self.timeline_objects = [] + gc.collect() + +bulk_creator = BulkCreator() + + +def custom_add_to_object_timeline(obj:object, instance:object, event_type:str, namespace:str="default", extra_data:dict={}): + assert isinstance(obj, Model), "obj must be a instance of Model" + assert isinstance(instance, Model), "instance must be a instance of Model" + event_type_key = _get_impl_key_from_model(instance.__class__, event_type) + impl = _timeline_impl_map.get(event_type_key, None) + + bulk_creator.create_element(Timeline( + content_object=obj, + namespace=namespace, + event_type=event_type_key, + project=None, + data=impl(instance, extra_data=extra_data), + data_content_type = ContentType.objects.get_for_model(instance.__class__), + created = bulk_creator.created, + )) + + +def generate_timeline(): + with patch('taiga.timeline.service._add_to_object_timeline', new=custom_add_to_object_timeline): + # Users api wasn't a HistoryResourceMixin so we can't interate on the HistoryEntries in this case + users = User.objects.order_by("date_joined") + for user in users.iterator(): + bulk_creator.created = user.date_joined + print("User:", user.date_joined) + extra_data = { + "values_diff": {}, + "user": extract_user_info(user), + } + _push_to_timelines(None, user, user, "create", extra_data=extra_data) + del extra_data + + bulk_creator.flush() + +class Command(BaseCommand): + help = 'Regenerate project timeline' + + def handle(self, *args, **options): + debug_enabled = settings.DEBUG + if debug_enabled: + print("Please, execute this script only with DEBUG mode disabled (DEBUG=False)") + return + + generate_timeline() diff --git a/taiga/timeline/migrations/0004_auto_20150603_1312.py b/taiga/timeline/migrations/0004_auto_20150603_1312.py new file mode 100644 index 00000000..5764b682 --- /dev/null +++ b/taiga/timeline/migrations/0004_auto_20150603_1312.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('timeline', '0003_auto_20150410_0829'), + ] + + operations = [ + migrations.AlterField( + model_name='timeline', + name='project', + field=models.ForeignKey(null=True, to='projects.Project'), + preserve_default=True, + ), + ] diff --git a/taiga/timeline/models.py b/taiga/timeline/models.py index 5a070c49..ac181842 100644 --- a/taiga/timeline/models.py +++ b/taiga/timeline/models.py @@ -31,7 +31,7 @@ class Timeline(models.Model): content_object = GenericForeignKey('content_type', 'object_id') namespace = models.CharField(max_length=250, default="default", db_index=True) event_type = models.CharField(max_length=250, db_index=True) - project = models.ForeignKey(Project) + project = models.ForeignKey(Project, null=True) data = JsonField() data_content_type = models.ForeignKey(ContentType, related_name="data_timelines") created = models.DateTimeField(default=timezone.now) diff --git a/taiga/timeline/serializers.py b/taiga/timeline/serializers.py index 20a266df..903dc00d 100644 --- a/taiga/timeline/serializers.py +++ b/taiga/timeline/serializers.py @@ -43,6 +43,7 @@ class TimelineDataJsonField(serializers.WritableField): "photo": get_photo_or_gravatar_url(user), "big_photo": get_big_photo_or_gravatar_url(user), "username": user.username, + "date_joined": user.date_joined, } except User.DoesNotExist: pass diff --git a/taiga/timeline/service.py b/taiga/timeline/service.py index 1586138d..5317942f 100644 --- a/taiga/timeline/service.py +++ b/taiga/timeline/service.py @@ -57,11 +57,15 @@ def _add_to_object_timeline(obj:object, instance:object, event_type:str, namespa event_type_key = _get_impl_key_from_model(instance.__class__, event_type) impl = _timeline_impl_map.get(event_type_key, None) + project = None + if hasattr(instance, "project"): + project = instance.project + Timeline.objects.create( content_object=obj, namespace=namespace, event_type=event_type_key, - project=instance.project, + project=project, data=impl(instance, extra_data=extra_data), data_content_type = ContentType.objects.get_for_model(instance.__class__), ) @@ -96,8 +100,8 @@ def get_timeline(obj, namespace=None): def filter_timeline_for_user(timeline, user): - # Filtering public projects - tl_filter = Q(project__is_private=False) + # Filtering entities from public projects or entities without project + tl_filter = Q(project__is_private=False) | Q(project=None) # Filtering private project with some public parts content_types = { diff --git a/taiga/timeline/signals.py b/taiga/timeline/signals.py index 549e54ed..9cc144e9 100644 --- a/taiga/timeline/signals.py +++ b/taiga/timeline/signals.py @@ -34,10 +34,11 @@ def _push_to_timeline(*args, **kwargs): def _push_to_timelines(project, user, obj, event_type, extra_data={}): + if project is not None: # Project timeline - _push_to_timeline(project, obj, event_type, - namespace=build_project_namespace(project), - extra_data=extra_data) + _push_to_timeline(project, obj, event_type, + namespace=build_project_namespace(project), + extra_data=extra_data) # User timeline _push_to_timeline(user, obj, event_type, @@ -56,18 +57,18 @@ def _push_to_timelines(project, user, obj, event_type, extra_data={}): if watchers: related_people |= watchers - # 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 + 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 + related_people = related_people.distinct() - related_people = related_people.distinct() + _push_to_timeline(related_people, obj, event_type, + namespace=build_user_namespace(user), + extra_data=extra_data) - _push_to_timeline(related_people, obj, event_type, - namespace=build_user_namespace(user), - extra_data=extra_data) - - #Related people: team members + #Related people: team members def on_new_history_entry(sender, instance, created, **kwargs): @@ -122,3 +123,10 @@ def create_membership_push_to_timeline(sender, instance, **kwargs): def delete_membership_push_to_timeline(sender, instance, **kwargs): if instance.user: _push_to_timelines(instance.project, instance.user, instance, "delete") + + +def create_user_push_to_timeline(sender, instance, created, **kwargs): + if created: + project = None + user = instance + _push_to_timelines(project, user, user, "create") diff --git a/taiga/timeline/timeline_implementations.py b/taiga/timeline/timeline_implementations.py index 8a782ce1..2b911607 100644 --- a/taiga/timeline/timeline_implementations.py +++ b/taiga/timeline/timeline_implementations.py @@ -105,3 +105,11 @@ def membership_timeline(instance, extra_data={}): } result.update(extra_data) return result + +@register_timeline_implementation("users.user", "create") +def user_timeline(instance, extra_data={}): + result = { + "user": service.extract_user_info(instance), + } + result.update(extra_data) + return result diff --git a/tests/integration/test_timeline.py b/tests/integration/test_timeline.py index 47583c70..326a7b85 100644 --- a/tests/integration/test_timeline.py +++ b/tests/integration/test_timeline.py @@ -36,7 +36,7 @@ def test_add_to_object_timeline(): service._add_to_object_timeline(user1, task, "test") - assert Timeline.objects.filter(object_id=user1.id).count() == 1 + assert Timeline.objects.filter(object_id=user1.id).count() == 2 assert Timeline.objects.order_by("-id")[0].data == id(task) @@ -59,9 +59,9 @@ def test_get_timeline(): service._add_to_object_timeline(user1, task4, "test") service._add_to_object_timeline(user2, task1, "test") - assert Timeline.objects.filter(object_id=user1.id).count() == 4 - assert Timeline.objects.filter(object_id=user2.id).count() == 1 - assert Timeline.objects.filter(object_id=user3.id).count() == 0 + assert Timeline.objects.filter(object_id=user1.id).count() == 5 + assert Timeline.objects.filter(object_id=user2.id).count() == 2 + assert Timeline.objects.filter(object_id=user3.id).count() == 1 def test_filter_timeline_no_privileges(): @@ -72,7 +72,7 @@ def test_filter_timeline_no_privileges(): service.register_timeline_implementation("tasks.task", "test", lambda x, extra_data=None: str(id(x))) service._add_to_object_timeline(user1, task1, "test") - timeline = Timeline.objects.all() + timeline = Timeline.objects.exclude(event_type="users.user.create") timeline = service.filter_timeline_for_user(timeline, user2) assert timeline.count() == 0 @@ -88,7 +88,7 @@ def test_filter_timeline_public_project(): service.register_timeline_implementation("tasks.task", "test", lambda x, extra_data=None: str(id(x))) service._add_to_object_timeline(user1, task1, "test") service._add_to_object_timeline(user1, task2, "test") - timeline = Timeline.objects.all() + timeline = Timeline.objects.exclude(event_type="users.user.create") timeline = service.filter_timeline_for_user(timeline, user2) assert timeline.count() == 1 @@ -104,7 +104,7 @@ def test_filter_timeline_private_project_anon_permissions(): service.register_timeline_implementation("tasks.task", "test", lambda x, extra_data=None: str(id(x))) service._add_to_object_timeline(user1, task1, "test") service._add_to_object_timeline(user1, task2, "test") - timeline = Timeline.objects.all() + timeline = Timeline.objects.exclude(event_type="users.user.create") timeline = service.filter_timeline_for_user(timeline, user2) assert timeline.count() == 1 @@ -123,7 +123,7 @@ def test_filter_timeline_private_project_member_permissions(): service.register_timeline_implementation("tasks.task", "test", lambda x, extra_data=None: str(id(x))) service._add_to_object_timeline(user1, task1, "test") service._add_to_object_timeline(user1, task2, "test") - timeline = Timeline.objects.all() + timeline = Timeline.objects.exclude(event_type="users.user.create") timeline = service.filter_timeline_for_user(timeline, user2) assert timeline.count() == 3