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
def project_values(diff):
values = _common_users_values(diff)
return values
def milestone_values(diff):
values = _common_users_values(diff)
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("wiki.wikipage", wikipage_freezer)
from .freeze_impl import project_values
from .freeze_impl import milestone_values
from .freeze_impl import userstory_values
from .freeze_impl import issue_values
from .freeze_impl import task_values
from .freeze_impl import wikipage_values
register_values_implementation("projects.project", project_values)
register_values_implementation("milestones.milestone", milestone_values)
register_values_implementation("userstories.userstory", userstory_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')
return self._get_user_stories_points(user_stories)
@property
def project(self):
return self
@property
def future_team_increment(self):
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 . import signals as handlers
from taiga.projects.history.models import HistoryEntry
class TimelineAppConfig(AppConfig):
@ -26,12 +27,7 @@ class TimelineAppConfig(AppConfig):
verbose_name = "Timeline"
def ready(self):
signals.post_save.connect(handlers.create_project_push_to_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.post_save.connect(handlers.on_new_history_entry, sender=HistoryEntry, dispatch_uid="timeline")
signals.pre_save.connect(handlers.create_membership_push_to_timeline,
sender=apps.get_model("projects", "Membership"))
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.generic import GenericForeignKey
from taiga.projects.models import Project
class Timeline(models.Model):
content_type = models.ForeignKey(ContentType)
content_type = models.ForeignKey(ContentType, related_name="content_type_timelines")
object_id = models.PositiveIntegerField()
content_object = GenericForeignKey('content_type', 'object_id')
namespace = models.SlugField(default="default")
event_type = models.SlugField()
project = models.ForeignKey(Project)
data = JsonField()
data_content_type = models.ForeignKey(ContentType, related_name="data_timelines")
created = models.DateTimeField(default=timezone.now)
def save(self, *args, **kwargs):

View File

@ -14,12 +14,16 @@
# 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.db.models import Model
from django.db.models.query import QuerySet
from functools import partial, wraps
from django.apps import apps
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.celery import app
_timeline_impl_map = {}
@ -37,22 +41,28 @@ def _get_impl_key_from_typename(typename:str, event_type:str):
raise Exception("Not valid typename parameter")
def _get_class_implementation(model:Model, event_type:str):
key = _get_impl_key_from_model(model, event_type)
return _timeline_impl_map.get(key, None)
def build_user_namespace(user:object):
return "{0}:{1}".format("user", user.id)
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={}):
assert isinstance(obj, Model), "obj must be a instance of Model"
assert isinstance(instance, Model), "instance must be a instance of Model"
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(
content_object=obj,
namespace=namespace,
event_type=event_type,
data=impl(instance, extra_data=extra_data)
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__),
)
@ -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)
@app.task
def push_to_timeline(objects, instance:object, event_type:str, namespace:str="default", extra_data:dict={}):
if isinstance(objects, Model):
_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")
def get_timeline(obj, namespace="default"):
def get_timeline(obj, namespace=None):
assert isinstance(obj, Model), "obj must be a instance of Model"
from .models import Timeline
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):

View File

@ -14,43 +14,90 @@
# 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 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 project watchers timeline when project watchers are implemented.
def create_project_push_to_timeline(sender, instance, created, **kwargs):
if created:
push_to_timeline(instance, instance, "create")
def _push_to_timeline(*args, **kwargs):
if settings.CELERY_ENABLED:
push_to_timeline.delay(*args, **kwargs)
else:
push_to_timeline(*args, **kwargs)
def create_user_story_push_to_timeline(sender, instance, created, **kwargs):
if created:
push_to_timeline(instance.project, instance, "create")
def _push_to_timelines(project, user, obj, event_type, extra_data={}):
# Project timeline
_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):
if created:
push_to_timeline(instance.project, instance, "create")
def on_new_history_entry(sender, instance, created, **kwargs):
if instance.is_hidden:
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):
# Creating new membership with associated 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:
prev_instance = sender.objects.get(pk=instance.pk)
if prev_instance.user != prev_instance.user:
push_to_timeline(instance.project, instance, "create")
elif prev_instance.role != prev_instance.role:
extra_data = {
"prev_role": {
"id": prev_instance.role.pk,
"name": prev_instance.role.name,
}
}
push_to_timeline(instance.project, instance, "role-changed", extra_data=extra_data)
if instance.user != prev_instance.user:
# The new member
_push_to_timelines(instance.project, instance.user, instance, "create")
# 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")
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")
def project_create_timeline(instance, extra_data={}):
return {
@register_timeline_implementation("projects.project", "change")
@register_timeline_implementation("projects.project", "delete")
def project_timeline(instance, extra_data={}):
result ={
"project": {
"id": instance.pk,
"slug": instance.slug,
"name": instance.name,
},
"creator": {
"id": instance.owner.pk,
"name": instance.owner.get_full_name(),
}
result.update(extra_data)
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")
def userstory_create_timeline(instance, extra_data={}):
return {
@register_timeline_implementation("userstories.userstory", "change")
@register_timeline_implementation("userstories.userstory", "delete")
def userstory_timeline(instance, extra_data={}):
result ={
"userstory": {
"id": instance.pk,
"subject": instance.subject,
@ -43,17 +65,17 @@ def userstory_create_timeline(instance, extra_data={}):
"id": instance.project.pk,
"slug": instance.project.slug,
"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")
def issue_create_timeline(instance, extra_data={}):
return {
@register_timeline_implementation("issues.issue", "change")
@register_timeline_implementation("issues.issue", "delete")
def issue_timeline(instance, extra_data={}):
result ={
"issue": {
"id": instance.pk,
"subject": instance.subject,
@ -62,62 +84,53 @@ def issue_create_timeline(instance, extra_data={}):
"id": instance.project.pk,
"slug": instance.project.slug,
"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": {
"id": instance.owner.pk,
"name": instance.owner.get_full_name(),
"project": {
"id": instance.project.pk,
"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")
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")
def membership_delete_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={}):
def membership_create_timeline(instance, extra_data={}):
result = {
"user": {
"id": instance.user.pk,
@ -128,9 +141,6 @@ def membership_role_changed_timeline(instance, extra_data={}):
"slug": instance.project.slug,
"name": instance.project.name,
},
"role": {
"id": instance.role.pk,
"name": instance.role.name,
}
}
return dict(result.items() + extra_data.items())
result.update(extra_data)
return result