Refactoring timeline

remotes/origin/enhancement/email-actions
Alejandro Alonso 2015-04-06 12:34:41 +02:00 committed by David Barragán Merino
parent 872e274825
commit 982d79d8aa
9 changed files with 363 additions and 113 deletions

View File

@ -96,10 +96,13 @@ def _common_users_values(diff):
return values return values
def project_values(diff):
values = _common_users_values(diff)
return values
def milestone_values(diff): def milestone_values(diff):
values = _common_users_values(diff) values = _common_users_values(diff)
return values return values

View File

@ -367,12 +367,14 @@ register_freeze_implementation("issues.issue", issue_freezer)
register_freeze_implementation("tasks.task", task_freezer) register_freeze_implementation("tasks.task", task_freezer)
register_freeze_implementation("wiki.wikipage", wikipage_freezer) register_freeze_implementation("wiki.wikipage", wikipage_freezer)
from .freeze_impl import project_values
from .freeze_impl import milestone_values from .freeze_impl import milestone_values
from .freeze_impl import userstory_values from .freeze_impl import userstory_values
from .freeze_impl import issue_values from .freeze_impl import issue_values
from .freeze_impl import task_values from .freeze_impl import task_values
from .freeze_impl import wikipage_values from .freeze_impl import wikipage_values
register_values_implementation("projects.project", project_values)
register_values_implementation("milestones.milestone", milestone_values) register_values_implementation("milestones.milestone", milestone_values)
register_values_implementation("userstories.userstory", userstory_values) register_values_implementation("userstories.userstory", userstory_values)
register_values_implementation("issues.issue", issue_values) register_values_implementation("issues.issue", issue_values)

View File

@ -281,6 +281,11 @@ class Project(ProjectDefaults, TaggedMixin, models.Model):
user_stories = user_stories.prefetch_related('role_points', 'role_points__points') user_stories = user_stories.prefetch_related('role_points', 'role_points__points')
return self._get_user_stories_points(user_stories) return self._get_user_stories_points(user_stories)
@property
def project(self):
return self
@property @property
def future_team_increment(self): def future_team_increment(self):
team_increment = self._get_points_increment(False, True) team_increment = self._get_points_increment(False, True)

View File

@ -19,6 +19,7 @@ from django.apps import apps
from django.db.models import signals from django.db.models import signals
from . import signals as handlers from . import signals as handlers
from taiga.projects.history.models import HistoryEntry
class TimelineAppConfig(AppConfig): class TimelineAppConfig(AppConfig):
@ -26,12 +27,7 @@ class TimelineAppConfig(AppConfig):
verbose_name = "Timeline" verbose_name = "Timeline"
def ready(self): def ready(self):
signals.post_save.connect(handlers.create_project_push_to_timeline, signals.post_save.connect(handlers.on_new_history_entry, sender=HistoryEntry, dispatch_uid="timeline")
sender=apps.get_model("projects", "Project"))
signals.post_save.connect(handlers.create_user_story_push_to_timeline,
sender=apps.get_model("userstories", "UserStory"))
signals.post_save.connect(handlers.create_issue_push_to_timeline,
sender=apps.get_model("issues", "Issue"))
signals.pre_save.connect(handlers.create_membership_push_to_timeline, signals.pre_save.connect(handlers.create_membership_push_to_timeline,
sender=apps.get_model("projects", "Membership")) sender=apps.get_model("projects", "Membership"))
signals.post_delete.connect(handlers.delete_membership_push_to_timeline, signals.post_delete.connect(handlers.delete_membership_push_to_timeline,

View File

@ -0,0 +1,112 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
from django.core.exceptions import ObjectDoesNotExist
from django.db.models import Model
from taiga.projects.models import Project
from taiga.projects.history import services as history_services
from taiga.projects.history.choices import HistoryType
from taiga.projects.history.models import HistoryEntry
from taiga.timeline.models import Timeline
from taiga.timeline.service import (_add_to_object_timeline, _get_impl_key_from_model,
_timeline_impl_map)
from taiga.timeline.signals import on_new_history_entry, _push_to_timelines
from taiga.users.models import User
from unittest.mock import patch
from django.contrib.contenttypes.models import ContentType
import django_pgjson.fields
import django.utils.timezone
timelime_objects = []
created = None
def custom_add_to_object_timeline(obj:object, instance:object, event_type:str, namespace:str="default", extra_data:dict={}):
global created
global timelime_objects
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)
timelime_objects.append(Timeline(
content_object=obj,
namespace=namespace,
event_type=event_type_key,
project=instance.project,
data=impl(instance, extra_data=extra_data),
data_content_type = ContentType.objects.get_for_model(instance.__class__),
created = created,
))
def generate_timeline(apps, schema_editor):
global created
global timelime_objects
with patch('taiga.timeline.service._add_to_object_timeline', new=custom_add_to_object_timeline):
# Projects api wasn't a HistoryResourceMixin so we can't interate on the HistoryEntries in this case
for project in Project.objects.order_by("created_date").iterator():
created = project.created_date
print("Project:", created)
extra_data = {
"values_diff": {},
"user": {
"pk": project.owner.id,
"user_name": project.owner.get_full_name(),
},
}
_push_to_timelines(project, project.owner, project, "create", extra_data=extra_data)
Timeline.objects.bulk_create(timelime_objects, batch_size=10000)
timelime_objects = []
for historyEntry in HistoryEntry.objects.order_by("created_at").iterator():
print("History entry:", historyEntry.created_at)
try:
created = historyEntry.created_at
on_new_history_entry(None, historyEntry, None)
except ObjectDoesNotExist as e:
print("Ignoring")
Timeline.objects.bulk_create(timelime_objects, batch_size=10000)
class Migration(migrations.Migration):
dependencies = [
('projects', '0019_auto_20150311_0821'),
('contenttypes', '0001_initial'),
('timeline', '0001_initial'),
]
operations = [
migrations.DeleteModel(
name='Timeline',
),
migrations.CreateModel(
name='Timeline',
fields=[
('id', models.AutoField(auto_created=True, verbose_name='ID', primary_key=True, serialize=False)),
('object_id', models.PositiveIntegerField()),
('namespace', models.SlugField(default='default')),
('event_type', models.SlugField()),
('project', models.ForeignKey(to='projects.Project')),
('data', django_pgjson.fields.JsonField()),
('data_content_type', models.ForeignKey(to='contenttypes.ContentType', related_name='data_timelines')),
('created', models.DateTimeField(default=django.utils.timezone.now)),
('content_type', models.ForeignKey(to='contenttypes.ContentType', related_name='content_type_timelines')),
],
options={
},
bases=(models.Model,),
),
migrations.AlterIndexTogether(
name='timeline',
index_together=set([('content_type', 'object_id', 'namespace')]),
),
migrations.RunPython(generate_timeline),
]

View File

@ -23,14 +23,17 @@ from django.core.exceptions import ValidationError
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes.generic import GenericForeignKey from django.contrib.contenttypes.generic import GenericForeignKey
from taiga.projects.models import Project
class Timeline(models.Model): class Timeline(models.Model):
content_type = models.ForeignKey(ContentType) content_type = models.ForeignKey(ContentType, related_name="content_type_timelines")
object_id = models.PositiveIntegerField() object_id = models.PositiveIntegerField()
content_object = GenericForeignKey('content_type', 'object_id') content_object = GenericForeignKey('content_type', 'object_id')
namespace = models.SlugField(default="default") namespace = models.SlugField(default="default")
event_type = models.SlugField() event_type = models.SlugField()
project = models.ForeignKey(Project)
data = JsonField() data = JsonField()
data_content_type = models.ForeignKey(ContentType, related_name="data_timelines")
created = models.DateTimeField(default=timezone.now) created = models.DateTimeField(default=timezone.now)
def save(self, *args, **kwargs): def save(self, *args, **kwargs):

View File

@ -14,12 +14,16 @@
# You should have received a copy of the GNU Affero General Public License # 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/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from django.db.models import Model from django.apps import apps
from django.db.models.query import QuerySet
from functools import partial, wraps
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.db.models import Model
from django.db.models import Q
from django.db.models.query import QuerySet
from functools import partial, wraps
from taiga.base.utils.db import get_typename_for_model_class from taiga.base.utils.db import get_typename_for_model_class
from taiga.celery import app
_timeline_impl_map = {} _timeline_impl_map = {}
@ -37,22 +41,28 @@ def _get_impl_key_from_typename(typename:str, event_type:str):
raise Exception("Not valid typename parameter") raise Exception("Not valid typename parameter")
def _get_class_implementation(model:Model, event_type:str): def build_user_namespace(user:object):
key = _get_impl_key_from_model(model, event_type) return "{0}:{1}".format("user", user.id)
return _timeline_impl_map.get(key, None)
def build_project_namespace(project:object):
return "{0}:{1}".format("project", project.id)
def _add_to_object_timeline(obj:object, instance:object, event_type:str, namespace:str="default", extra_data:dict={}): def _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(obj, Model), "obj must be a instance of Model"
assert isinstance(instance, Model), "instance must be a instance of Model" assert isinstance(instance, Model), "instance must be a instance of Model"
from .models import Timeline from .models import Timeline
event_type_key = _get_impl_key_from_model(instance.__class__, event_type)
impl = _timeline_impl_map.get(event_type_key, None)
impl = _get_class_implementation(instance.__class__, event_type)
Timeline.objects.create( Timeline.objects.create(
content_object=obj, content_object=obj,
namespace=namespace, namespace=namespace,
event_type=event_type, event_type=event_type_key,
data=impl(instance, extra_data=extra_data) project=instance.project,
data=impl(instance, extra_data=extra_data),
data_content_type = ContentType.objects.get_for_model(instance.__class__),
) )
@ -61,6 +71,7 @@ def _add_to_objects_timeline(objects, instance:object, event_type:str, namespace
_add_to_object_timeline(obj, instance, event_type, namespace, extra_data) _add_to_object_timeline(obj, instance, event_type, namespace, extra_data)
@app.task
def push_to_timeline(objects, instance:object, event_type:str, namespace:str="default", extra_data:dict={}): def push_to_timeline(objects, instance:object, event_type:str, namespace:str="default", extra_data:dict={}):
if isinstance(objects, Model): if isinstance(objects, Model):
_add_to_object_timeline(objects, instance, event_type, namespace, extra_data) _add_to_object_timeline(objects, instance, event_type, namespace, extra_data)
@ -70,12 +81,73 @@ def push_to_timeline(objects, instance:object, event_type:str, namespace:str="de
raise Exception("Invalid objects parameter") raise Exception("Invalid objects parameter")
def get_timeline(obj, namespace="default"): def get_timeline(obj, namespace=None):
assert isinstance(obj, Model), "obj must be a instance of Model" assert isinstance(obj, Model), "obj must be a instance of Model"
from .models import Timeline from .models import Timeline
ct = ContentType.objects.get_for_model(obj.__class__) ct = ContentType.objects.get_for_model(obj.__class__)
return Timeline.objects.filter(content_type=ct, object_id=obj.pk, namespace=namespace) timeline = Timeline.objects.filter(content_type=ct, object_id=obj.pk)
if namespace is not None:
timeline = timeline.filter(namespace=namespace)
timeline = timeline.order_by("-created")
return timeline
def filter_timeline_for_user(timeline, user):
# Filtering public projects
tl_filter = Q(project__is_private=False)
# Filtering private project with some public parts
content_types = {
"view_project": ContentType.objects.get(app_label="projects", model="project"),
"view_milestones": ContentType.objects.get(app_label="milestones", model="milestone"),
"view_us": ContentType.objects.get(app_label="userstories", model="userstory"),
"view_tasks": ContentType.objects.get(app_label="tasks", model="task"),
"view_issues": ContentType.objects.get(app_label="issues", model="issue"),
"view_wiki_pages": ContentType.objects.get(app_label="wiki", model="wikipage"),
"view_wiki_links": ContentType.objects.get(app_label="wiki", model="wikilink"),
}
for content_type_key, content_type in content_types.items():
tl_filter |= Q(project__is_private=True,
project__anon_permissions__contains=[content_type_key],
data_content_type=content_type)
# Filtering private projects where user is member
if not user.is_anonymous():
membership_model = apps.get_model('projects', 'Membership')
memberships_qs = membership_model.objects.filter(user=user)
for membership in memberships_qs:
for content_type_key, content_type in content_types.items():
if content_type_key in membership.role.permissions or membership.is_owner:
tl_filter |= Q(project=membership.project, data_content_type=content_type)
timeline = timeline.filter(tl_filter)
return timeline
def get_profile_timeline(user, accessing_user=None):
timeline = get_timeline(user)
if accessing_user is not None:
timeline = filter_timeline_for_user(timeline, accessing_user)
return timeline
def get_user_timeline(user, accessing_user=None):
namespace = build_user_namespace(user)
timeline = get_timeline(user, namespace)
if accessing_user is not None:
timeline = filter_timeline_for_user(timeline, accessing_user)
return timeline
def get_project_timeline(project, accessing_user=None):
namespace = build_project_namespace(project)
timeline = get_timeline(project, namespace)
if accessing_user is not None:
timeline = filter_timeline_for_user(timeline, accessing_user)
return timeline
def register_timeline_implementation(typename:str, event_type:str, fn=None): def register_timeline_implementation(typename:str, event_type:str, fn=None):

View File

@ -14,43 +14,90 @@
# You should have received a copy of the GNU Affero General Public License # 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/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from taiga.timeline.service import push_to_timeline from django.conf import settings
from taiga.projects.history import services as history_services
from taiga.projects.models import Project
from taiga.users.models import User
from taiga.projects.history.choices import HistoryType
from taiga.timeline.service import push_to_timeline, build_user_namespace, build_project_namespace
# TODO: Add events to followers timeline when followers are implemented. # TODO: Add events to followers timeline when followers are implemented.
# TODO: Add events to project watchers timeline when project watchers are implemented. # TODO: Add events to project watchers timeline when project watchers are implemented.
def _push_to_timeline(*args, **kwargs):
def create_project_push_to_timeline(sender, instance, created, **kwargs): if settings.CELERY_ENABLED:
if created: push_to_timeline.delay(*args, **kwargs)
push_to_timeline(instance, instance, "create") else:
push_to_timeline(*args, **kwargs)
def create_user_story_push_to_timeline(sender, instance, created, **kwargs): def _push_to_timelines(project, user, obj, event_type, extra_data={}):
if created: # Project timeline
push_to_timeline(instance.project, instance, "create") _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,
namespace=build_user_namespace(user),
extra_data=extra_data)
# Related people: watchers and assigned to
if hasattr(obj, "assigned_to") and obj.assigned_to and user != obj.assigned_to:
_push_to_timeline(obj.assigned_to, obj, event_type,
namespace=build_user_namespace(user),
extra_data=extra_data)
watchers = hasattr(obj, "watchers") and obj.watchers.exclude(id=user.id) or []
if watchers:
_push_to_timeline(watchers, obj, event_type,
namespace=build_user_namespace(user),
extra_data=extra_data)
def create_issue_push_to_timeline(sender, instance, created, **kwargs): def on_new_history_entry(sender, instance, created, **kwargs):
if created: if instance.is_hidden:
push_to_timeline(instance.project, instance, "create") return None
model = history_services.get_model_from_key(instance.key)
pk = history_services.get_pk_from_key(instance.key)
obj = model.objects.get(pk=pk)
project = obj.project
if instance.type == HistoryType.create:
event_type = "create"
elif instance.type == HistoryType.change:
event_type = "change"
elif instance.type == HistoryType.delete:
event_type = "delete"
extra_data = {
"values_diff": instance.values_diff,
"user": instance.user,
"comment": instance.comment,
}
user = User.objects.get(id=instance.user["pk"])
_push_to_timelines(project, user, obj, event_type, extra_data=extra_data)
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 not instance.pk and instance.user: if not instance.pk and instance.user:
push_to_timeline(instance.project, instance, "create") _push_to_timelines(instance.project, instance.user, instance, "create")
#Updating existing membership
elif instance.pk: elif instance.pk:
prev_instance = sender.objects.get(pk=instance.pk) prev_instance = sender.objects.get(pk=instance.pk)
if prev_instance.user != prev_instance.user: if instance.user != prev_instance.user:
push_to_timeline(instance.project, instance, "create") # The new member
elif prev_instance.role != prev_instance.role: _push_to_timelines(instance.project, instance.user, instance, "create")
extra_data = { # If we are updating the old user is removed from project
"prev_role": { if prev_instance.user:
"id": prev_instance.role.pk, _push_to_timelines(instance.project, prev_instance.user, prev_instance, "delete")
"name": prev_instance.role.name,
}
}
push_to_timeline(instance.project, instance, "role-changed", extra_data=extra_data)
def delete_membership_push_to_timeline(sender, instance, **kwargs): def delete_membership_push_to_timeline(sender, instance, **kwargs):
push_to_timeline(instance.project, instance, "delete") if instance.user:
_push_to_timelines(instance.project, instance.user, instance, "delete")

View File

@ -18,23 +18,45 @@ from taiga.timeline.service import register_timeline_implementation
@register_timeline_implementation("projects.project", "create") @register_timeline_implementation("projects.project", "create")
def project_create_timeline(instance, extra_data={}): @register_timeline_implementation("projects.project", "change")
return { @register_timeline_implementation("projects.project", "delete")
def project_timeline(instance, extra_data={}):
result ={
"project": { "project": {
"id": instance.pk, "id": instance.pk,
"slug": instance.slug, "slug": instance.slug,
"name": instance.name, "name": instance.name,
}, },
"creator": { }
"id": instance.owner.pk, result.update(extra_data)
"name": instance.owner.get_full_name(), return result
@register_timeline_implementation("milestones.milestone", "create")
@register_timeline_implementation("milestones.milestone", "change")
@register_timeline_implementation("milestones.milestone", "delete")
def project_timeline(instance, extra_data={}):
result ={
"milestone": {
"id": instance.pk,
"slug": instance.slug,
"name": instance.name,
},
"project": {
"id": instance.project.pk,
"slug": instance.project.slug,
"name": instance.project.name,
} }
} }
result.update(extra_data)
return result
@register_timeline_implementation("userstories.userstory", "create") @register_timeline_implementation("userstories.userstory", "create")
def userstory_create_timeline(instance, extra_data={}): @register_timeline_implementation("userstories.userstory", "change")
return { @register_timeline_implementation("userstories.userstory", "delete")
def userstory_timeline(instance, extra_data={}):
result ={
"userstory": { "userstory": {
"id": instance.pk, "id": instance.pk,
"subject": instance.subject, "subject": instance.subject,
@ -43,17 +65,17 @@ def userstory_create_timeline(instance, extra_data={}):
"id": instance.project.pk, "id": instance.project.pk,
"slug": instance.project.slug, "slug": instance.project.slug,
"name": instance.project.name, "name": instance.project.name,
},
"creator": {
"id": instance.owner.pk,
"name": instance.owner.get_full_name(),
} }
} }
result.update(extra_data)
return result
@register_timeline_implementation("issues.issue", "create") @register_timeline_implementation("issues.issue", "create")
def issue_create_timeline(instance, extra_data={}): @register_timeline_implementation("issues.issue", "change")
return { @register_timeline_implementation("issues.issue", "delete")
def issue_timeline(instance, extra_data={}):
result ={
"issue": { "issue": {
"id": instance.pk, "id": instance.pk,
"subject": instance.subject, "subject": instance.subject,
@ -62,62 +84,53 @@ def issue_create_timeline(instance, extra_data={}):
"id": instance.project.pk, "id": instance.project.pk,
"slug": instance.project.slug, "slug": instance.project.slug,
"name": instance.project.name, "name": instance.project.name,
}
}
result.update(extra_data)
return result
@register_timeline_implementation("tasks.task", "create")
@register_timeline_implementation("tasks.task", "change")
@register_timeline_implementation("tasks.task", "delete")
def task_timeline(instance, extra_data={}):
result ={
"task": {
"id": instance.pk,
"subject": instance.subject,
}, },
"creator": { "project": {
"id": instance.owner.pk, "id": instance.project.pk,
"name": instance.owner.get_full_name(), "slug": instance.project.slug,
"name": instance.project.name,
} }
} }
result.update(extra_data)
return result
@register_timeline_implementation("wiki.wikipage", "create")
@register_timeline_implementation("wiki.wikipage", "change")
@register_timeline_implementation("wiki.wikipage", "delete")
def wiki_page_timeline(instance, extra_data={}):
result ={
"wiki_page": {
"id": instance.pk,
"slug": instance.slug,
},
"project": {
"id": instance.project.pk,
"slug": instance.project.slug,
"name": instance.project.name,
}
}
result.update(extra_data)
return result
@register_timeline_implementation("projects.membership", "create") @register_timeline_implementation("projects.membership", "create")
def membership_create_timeline(instance, extra_data={}):
return {
"user": {
"id": instance.user.pk,
"name": instance.user.get_full_name(),
},
"project": {
"id": instance.project.pk,
"slug": instance.project.slug,
"name": instance.project.name,
},
"role": {
"id": instance.role.pk,
"name": instance.role.name,
}
}
@register_timeline_implementation("projects.membership", "delete") @register_timeline_implementation("projects.membership", "delete")
def membership_delete_timeline(instance, extra_data={}): def membership_create_timeline(instance, extra_data={}):
if instance.user:
return {
"user": {
"id": instance.user.pk,
"name": instance.user.get_full_name(),
},
"project": {
"id": instance.project.pk,
"slug": instance.project.slug,
"name": instance.project.name,
},
}
return {
"invitation": {
"id": instance.pk,
"email": instance.email,
},
"project": {
"id": instance.project.pk,
"slug": instance.project.slug,
"name": instance.project.name,
},
}
@register_timeline_implementation("projects.membership", "role-changed")
def membership_role_changed_timeline(instance, extra_data={}):
result = { result = {
"user": { "user": {
"id": instance.user.pk, "id": instance.user.pk,
@ -128,9 +141,6 @@ def membership_role_changed_timeline(instance, extra_data={}):
"slug": instance.project.slug, "slug": instance.project.slug,
"name": instance.project.name, "name": instance.project.name,
}, },
"role": {
"id": instance.role.pk,
"name": instance.role.name,
} }
} result.update(extra_data)
return dict(result.items() + extra_data.items()) return result