Working in history and timeline for epics (initial version)

remotes/origin/issue/4795/notification_even_they_are_disabled
David Barragán Merino 2016-07-01 12:37:19 +02:00
parent 389a18026b
commit a7c262ffdc
16 changed files with 173 additions and 50 deletions

View File

@ -168,6 +168,11 @@ class HistoryViewSet(ReadOnlyListViewSet):
return self.response_for_queryset(qs)
class EpicHistory(HistoryViewSet):
content_type = "epics.epic"
permission_classes = (permissions.EpicHistoryPermission,)
class UserStoryHistory(HistoryViewSet):
content_type = "userstories.userstory"
permission_classes = (permissions.UserStoryHistoryPermission,)

View File

@ -106,6 +106,17 @@ def milestone_values(diff):
return values
def epic_values(diff):
values = _common_users_values(diff)
if "status" in diff:
values["status"] = _get_us_status_values(diff["status"])
# TODO EPICS: What happen with usr stories?
return values
def userstory_values(diff):
values = _common_users_values(diff)
@ -190,6 +201,18 @@ def extract_attachments(obj) -> list:
"order": attach.order}
@as_tuple
def extract_epic_custom_attributes(obj) -> list:
with suppress(ObjectDoesNotExist):
custom_attributes_values = obj.custom_attributes_values.attributes_values
for attr in obj.project.epiccustomattributes.all():
with suppress(KeyError):
value = custom_attributes_values[str(attr.id)]
yield {"id": attr.id,
"name": attr.name,
"value": value}
@as_tuple
def extract_user_story_custom_attributes(obj) -> list:
with suppress(ObjectDoesNotExist):
@ -235,6 +258,7 @@ def project_freezer(project) -> dict:
"total_milestones",
"total_story_points",
"tags",
"is_epics_activated",
"is_backlog_activated",
"is_kanban_activated",
"is_wiki_activated",
@ -256,6 +280,31 @@ def milestone_freezer(milestone) -> dict:
return snapshot
def epic_freezer(epic) -> dict:
snapshot = {
"ref": epic.ref,
"owner": epic.owner_id,
"status": epic.status.id if epic.status else None,
"is_closed": epic.is_closed,
"finish_date": str(epic.finish_date),
"epics_order": epic.epics_order,
"subject": epic.subject,
"description": epic.description,
"description_html": mdrender(epic.project, epic.description),
"assigned_to": epic.assigned_to_id,
"client_requirement": epic.client_requirement,
"team_requirement": epic.team_requirement,
"attachments": extract_attachments(epic),
"tags": epic.tags,
"is_blocked": epic.is_blocked,
"blocked_note": epic.blocked_note,
"blocked_note_html": mdrender(epic.project, epic.blocked_note),
"custom_attributes": extract_epic_custom_attributes(epic),
}
return snapshot
def userstory_freezer(us) -> dict:
rp_cls = apps.get_model("userstories", "RolePoints")
rpqsd = rp_cls.objects.filter(user_story=us)

View File

@ -42,6 +42,14 @@ class IsCommentProjectAdmin(PermissionComponent):
return is_project_admin(request.user, project)
class EpicHistoryPermission(TaigaResourcePermission):
retrieve_perms = HasProjectPerm('view_project')
edit_comment_perms = IsCommentProjectAdmin() | IsCommentOwner()
delete_comment_perms = IsCommentProjectAdmin() | IsCommentOwner()
undelete_comment_perms = IsCommentProjectAdmin() | IsCommentDeleter()
comment_versions_perms = IsCommentProjectAdmin() | IsCommentOwner()
class UserStoryHistoryPermission(TaigaResourcePermission):
retrieve_perms = HasProjectPerm('view_project')
edit_comment_perms = IsCommentProjectAdmin() | IsCommentOwner()

View File

@ -50,6 +50,7 @@ from .models import HistoryType
# Freeze implementatitions
from .freeze_impl import project_freezer
from .freeze_impl import milestone_freezer
from .freeze_impl import epic_freezer
from .freeze_impl import userstory_freezer
from .freeze_impl import issue_freezer
from .freeze_impl import task_freezer
@ -58,6 +59,7 @@ from .freeze_impl import wikipage_freezer
from .freeze_impl import project_values
from .freeze_impl import milestone_values
from .freeze_impl import epic_values
from .freeze_impl import userstory_values
from .freeze_impl import issue_values
from .freeze_impl import task_values
@ -337,10 +339,7 @@ def take_snapshot(obj: object, *, comment: str="", user=None, delete: bool=False
# If diff and comment are empty, do
# not create empty history entry
if (not fdiff.diff and not comment
and old_fobj is not None
and entry_type != HistoryType.delete):
if (not fdiff.diff and not comment and old_fobj is not None and entry_type != HistoryType.delete):
return None
fvals = make_diff_values(typename, fdiff)
@ -394,8 +393,10 @@ def prefetch_owners_in_history_queryset(qs):
return qs
# Freeze & value register
register_freeze_implementation("projects.project", project_freezer)
register_freeze_implementation("milestones.milestone", milestone_freezer,)
register_freeze_implementation("epics.epic", epic_freezer)
register_freeze_implementation("userstories.userstory", userstory_freezer)
register_freeze_implementation("issues.issue", issue_freezer)
register_freeze_implementation("tasks.task", task_freezer)
@ -403,6 +404,7 @@ register_freeze_implementation("wiki.wikipage", wikipage_freezer)
register_values_implementation("projects.project", project_values)
register_values_implementation("milestones.milestone", milestone_values)
register_values_implementation("epics.epic", epic_values)
register_values_implementation("userstories.userstory", userstory_values)
register_values_implementation("issues.issue", issue_values)
register_values_implementation("tasks.task", task_values)

View File

@ -30,15 +30,6 @@ from taiga.permissions.services import calculate_permissions
from taiga.permissions.services import is_project_admin, is_project_owner
from . import services
<<<<<<< HEAD
=======
from .custom_attributes.serializers import EpicCustomAttributeSerializer
from .custom_attributes.serializers import UserStoryCustomAttributeSerializer
from .custom_attributes.serializers import TaskCustomAttributeSerializer
from .custom_attributes.serializers import IssueCustomAttributeSerializer
from .likes.mixins.serializers import FanResourceSerializerMixin
from .mixins.serializers import ValidateDuplicatedNameInProjectMixin
>>>>>>> 5f3559d... Epic custom attributes values
from .notifications.choices import NotifyLevel
@ -222,6 +213,7 @@ class ProjectSerializer(serializers.LightSerializer):
members = MethodField()
total_milestones = Field()
total_story_points = Field()
is_epics_activated = Field()
is_backlog_activated = Field()
is_kanban_activated = Field()
is_wiki_activated = Field()
@ -249,6 +241,7 @@ class ProjectSerializer(serializers.LightSerializer):
tags = Field()
tags_colors = MethodField()
default_epic_status = Field(attr="default_epic_status_id")
default_points = Field(attr="default_points_id")
default_us_status = Field(attr="default_us_status_id")
default_task_status = Field(attr="default_task_status_id")
@ -300,8 +293,7 @@ class ProjectSerializer(serializers.LightSerializer):
def get_my_permissions(self, obj):
if "request" in self.context:
user = self.context["request"].user
return calculate_permissions(
is_authenticated=user.is_authenticated(),
return calculate_permissions(is_authenticated=user.is_authenticated(),
is_superuser=user.is_superuser,
is_member=self.get_i_am_member(obj),
is_admin=self.get_i_am_admin(obj),
@ -441,10 +433,8 @@ class ProjectDetailSerializer(ProjectSerializer):
return len(obj.members_attr)
def get_is_out_of_owner_limits(self, obj):
assert (hasattr(obj, "private_projects_same_owner_attr"),
"instance must have a private_projects_same_owner_attr attribute"
assert (hasattr(obj, "public_projects_same_owner_attr"),
"instance must have a public_projects_same_owner_attr attribute"
assert hasattr(obj, "private_projects_same_owner_attr"), "instance must have a private_projects_same_owner_attr attribute"
assert hasattr(obj, "public_projects_same_owner_attr"), "instance must have a public_projects_same_owner_attr attribute"
return services.check_if_project_is_out_of_owner_limits(
obj,
current_memberships=self.get_total_memberships(obj),
@ -453,10 +443,8 @@ class ProjectDetailSerializer(ProjectSerializer):
)
def get_is_private_extra_info(self, obj):
assert (hasattr(obj, "private_projects_same_owner_attr"),
"instance must have a private_projects_same_owner_attr attribute"
assert (hasattr(obj, "public_projects_same_owner_attr"),
"instance must have a public_projects_same_owner_attr attribute"
assert hasattr(obj, "private_projects_same_owner_attr"), "instance must have a private_projects_same_owner_attr attribute"
assert hasattr(obj, "public_projects_same_owner_attr"), "instance must have a public_projects_same_owner_attr attribute"
return services.check_if_project_privacity_can_be_changed(
obj,
current_memberships=self.get_total_memberships(obj),

View File

@ -196,11 +196,13 @@ router.register(r"wiki-links", WikiLinkViewSet,
# History & Components
from taiga.projects.history.api import EpicHistory
from taiga.projects.history.api import UserStoryHistory
from taiga.projects.history.api import TaskHistory
from taiga.projects.history.api import IssueHistory
from taiga.projects.history.api import WikiHistory
router.register(r"history/epic", EpicHistory, base_name="epic-history")
router.register(r"history/userstory", UserStoryHistory, base_name="userstory-history")
router.register(r"history/task", TaskHistory, base_name="task-history")
router.register(r"history/issue", IssueHistory, base_name="issue-history")

View File

@ -85,6 +85,7 @@ class TimelineViewSet(ReadOnlyListViewSet):
event_type::text = ANY('{issues.issue.change,
tasks.task.change,
userstories.userstory.change,
epics.epic.change,
wiki.wikipage.change}'::text[])
)
"""])
@ -92,6 +93,7 @@ class TimelineViewSet(ReadOnlyListViewSet):
qs = qs.exclude(event_type__in=["issues.issue.delete",
"tasks.task.delete",
"userstories.userstory.delete",
"epics.epic.delete",
"wiki.wikipage.delete",
"projects.project.change"])

View File

@ -25,8 +25,9 @@ from django.core.management.base import BaseCommand
from django.db.models import Model
from django.test.utils import override_settings
from taiga.timeline.service import (_get_impl_key_from_model,
_timeline_impl_map, extract_user_info)
from taiga.timeline.service import _get_impl_key_from_model,
from taiga.timeline.service import _timeline_impl_map,
from taiga.timeline.service import extract_user_info)
from taiga.timeline.models import Timeline
from taiga.timeline.signals import _push_to_timelines
@ -54,7 +55,8 @@ class BulkCreator(object):
bulk_creator = BulkCreator()
def custom_add_to_object_timeline(obj:object, instance:object, event_type:str, created_datetime:object, namespace:str="default", extra_data:dict={}):
def custom_add_to_object_timeline(obj:object, instance:object, event_type:str, created_datetime:object,
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)

View File

@ -40,7 +40,9 @@ def update_timeline(initial_date, final_date):
print("Generating tasks indexed by id dict")
task_ids = timelines.values_list("object_id", flat=True)
tasks_per_id = {task.id: task for task in Task.objects.filter(id__in=task_ids).select_related("user_story").iterator()}
tasks_iterator = Task.objects.filter(id__in=task_ids).select_related("user_story").iterator()
tasks_per_id = {task.id: task for task in tasks_iterator}
del task_ids
counter = 1

View File

@ -58,7 +58,8 @@ class BulkCreator(object):
bulk_creator = BulkCreator()
def custom_add_to_object_timeline(obj:object, instance:object, event_type:str, created_datetime:object, namespace:str="default", extra_data:dict={}):
def custom_add_to_object_timeline(obj:object, instance:object, event_type:str, created_datetime:object,
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)
@ -102,11 +103,13 @@ def generate_timeline(initial_date, final_date, project_id):
if project_id:
project = Project.objects.get(id=project_id)
us_keys = ['userstories.userstory:%s'%(id) for id in project.user_stories.values_list("id", flat=True)]
epic_keys = ['epics.epic:%s'%(id) for id in project.epics.values_list("id", flat=True)]
us_keys = ['userstories.userstory:%s'%(id) for id in project.user_stories.values_list("id",
flat=True)]
tasks_keys = ['tasks.task:%s'%(id) for id in project.tasks.values_list("id", flat=True)]
issue_keys = ['issues.issue:%s'%(id) for id in project.issues.values_list("id", flat=True)]
wiki_keys = ['wiki.wikipage:%s'%(id) for id in project.wiki_pages.values_list("id", flat=True)]
keys = us_keys + tasks_keys + issue_keys + wiki_keys
keys = epic_keys + us_keys + tasks_keys + issue_keys + wiki_keys
projects = projects.filter(id=project_id)
history_entries = history_entries.filter(key__in=keys)
@ -121,7 +124,8 @@ def generate_timeline(initial_date, final_date, project_id):
"values_diff": {},
"user": extract_user_info(project.owner),
}
_push_to_timelines(project, project.owner, project, "create", project.created_date, extra_data=extra_data)
_push_to_timelines(project, project.owner, project, "create", project.created_date,
extra_data=extra_data)
del extra_data
for historyEntry in history_entries.iterator():

View File

@ -31,7 +31,7 @@ class Command(BaseCommand):
total = Project.objects.count()
for count,project in enumerate(Project.objects.order_by("id")):
print("""***********************************
%s/%s %s
***********************************"""%(count+1, total, project.name))
print("***********************************\n",
" {}/{} {}\n".format(count+1, total, project.name),
"***********************************")
call_command("rebuild_timeline", project=project.id)

View File

@ -52,7 +52,8 @@ 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, created_datetime: object, namespace: str="default", extra_data: dict={}):
def _add_to_object_timeline(obj: object, instance: object, event_type: str, created_datetime: object,
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
@ -74,12 +75,14 @@ def _add_to_object_timeline(obj: object, instance: object, event_type: str, crea
)
def _add_to_objects_timeline(objects, instance: object, event_type: str, created_datetime: object, namespace: str="default", extra_data: dict={}):
def _add_to_objects_timeline(objects, instance: object, event_type: str, created_datetime: object,
namespace: str="default", extra_data: dict={}):
for obj in objects:
_add_to_object_timeline(obj, instance, event_type, created_datetime, namespace, extra_data)
def _push_to_timeline(objects, instance: object, event_type: str, created_datetime: object, namespace: str="default", extra_data: dict={}):
def _push_to_timeline(objects, instance: object, event_type: str, created_datetime: object,
namespace: str="default", extra_data: dict={}):
if isinstance(objects, Model):
_add_to_object_timeline(objects, instance, event_type, created_datetime, namespace, extra_data)
elif isinstance(objects, QuerySet) or isinstance(objects, list):
@ -89,7 +92,8 @@ def _push_to_timeline(objects, instance: object, event_type: str, created_dateti
@app.task
def push_to_timelines(project_id, user_id, obj_app_label, obj_model_name, obj_id, event_type, created_datetime, extra_data={}):
def push_to_timelines(project_id, user_id, obj_app_label, obj_model_name, obj_id, event_type,
created_datetime, extra_data={}):
ObjModel = apps.get_model(obj_app_label, obj_model_name)
try:
obj = ObjModel.objects.get(id=obj_id)
@ -156,6 +160,7 @@ def filter_timeline_for_user(timeline, user):
content_types = {
"view_project": ContentType.objects.get_by_natural_key("projects", "project"),
"view_milestones": ContentType.objects.get_by_natural_key("milestones", "milestone"),
"view_epic": ContentType.objects.get_by_natural_key("epics", "epic"),
"view_us": ContentType.objects.get_by_natural_key("userstories", "userstory"),
"view_tasks": ContentType.objects.get_by_natural_key("tasks", "task"),
"view_issues": ContentType.objects.get_by_natural_key("issues", "issue"),
@ -181,7 +186,8 @@ def filter_timeline_for_user(timeline, user):
if membership.is_admin:
tl_filter |= Q(project=membership.project)
else:
data_content_types = list(filter(None, [content_types.get(a, None) for a in membership.role.permissions]))
data_content_types = list(filter(None, [content_types.get(a, None) for a in
membership.role.permissions]))
data_content_types.append(membership_content_type)
tl_filter |= Q(project=membership.project, data_content_type__in=data_content_types)
@ -252,6 +258,14 @@ def extract_milestone_info(instance):
}
def extract_epic_info(instance):
return {
"id": instance.pk,
"ref": instance.ref,
"subject": instance.subject,
}
def extract_userstory_info(instance):
return {
"id": instance.pk,

View File

@ -36,9 +36,23 @@ def _push_to_timelines(project, user, obj, event_type, created_datetime, extra_d
ct = ContentType.objects.get_for_model(obj)
if settings.CELERY_ENABLED:
connection.on_commit(lambda: push_to_timelines.delay(project_id, user.id, ct.app_label, ct.model, obj.id, event_type, created_datetime, extra_data=extra_data))
connection.on_commit(lambda: push_to_timelines.delay(project_id,
user.id,
ct.app_label,
ct.model,
obj.id,
event_type,
created_datetime,
extra_data=extra_data))
else:
push_to_timelines(project_id, user.id, ct.app_label, ct.model, obj.id, event_type, created_datetime, extra_data=extra_data)
push_to_timelines(project_id,
user.id,
ct.app_label,
ct.model,
obj.id,
event_type,
created_datetime,
extra_data=extra_data)
def _clean_description_fields(values_diff):

View File

@ -43,6 +43,18 @@ def milestone_timeline(instance, extra_data={}):
return result
@register_timeline_implementation("epics.epic", "create")
@register_timeline_implementation("epics.epic", "change")
@register_timeline_implementation("epics.epic", "delete")
def epic_timeline(instance, extra_data={}):
result ={
"epic": service.extract_epic_info(instance),
"project": service.extract_project_info(instance.project),
}
result.update(extra_data)
return result
@register_timeline_implementation("userstories.userstory", "create")
@register_timeline_implementation("userstories.userstory", "change")
@register_timeline_implementation("userstories.userstory", "delete")

View File

@ -389,6 +389,16 @@ class IssueTypeFactory(Factory):
project = factory.SubFactory("tests.factories.ProjectFactory")
class EpicCustomAttributeFactory(Factory):
class Meta:
model = "custom_attributes.EpicCustomAttribute"
strategy = factory.CREATE_STRATEGY
name = factory.Sequence(lambda n: "Epic Custom Attribute {}".format(n))
description = factory.Sequence(lambda n: "Description for Epic Custom Attribute {}".format(n))
project = factory.SubFactory("tests.factories.ProjectFactory")
class UserStoryCustomAttributeFactory(Factory):
class Meta:
model = "custom_attributes.UserStoryCustomAttribute"
@ -419,6 +429,15 @@ class IssueCustomAttributeFactory(Factory):
project = factory.SubFactory("tests.factories.ProjectFactory")
class EpicCustomAttributesValuesFactory(Factory):
class Meta:
model = "custom_attributes.EpicCustomAttributesValues"
strategy = factory.CREATE_STRATEGY
attributes_values = {}
epic = factory.SubFactory("tests.factories.EpicFactory")
class UserStoryCustomAttributesValuesFactory(Factory):
class Meta:
model = "custom_attributes.UserStoryCustomAttributesValues"