diff --git a/taiga/importers/management/commands/import_from_pivotal.py b/taiga/importers/management/commands/import_from_pivotal.py new file mode 100644 index 00000000..f3430c69 --- /dev/null +++ b/taiga/importers/management/commands/import_from_pivotal.py @@ -0,0 +1,93 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino +# Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso +# 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 . + +from django.core.management.base import BaseCommand +from django.db.models import Q + +from taiga.importers.pivotal import PivotalImporter +from taiga.users.models import User +from taiga.projects.services import projects as service + +import unittest.mock +import timeit + + +class Command(BaseCommand): + def add_arguments(self, parser): + parser.add_argument('--token', dest="token", type=str, + help='Auth token') + parser.add_argument('--project-id', dest="project_id", type=str, + help='Project ID or full name (ex: taigaio/taiga-back)') + parser.add_argument('--template', dest='template', default="scrum", + help='template to use: scrum or scrum (default scrum)') + parser.add_argument('--ask-for-users', dest='ask_for_users', const=True, + action="store_const", default=False, + help='Import closed data') + parser.add_argument('--closed-data', dest='closed_data', const=True, + action="store_const", default=False, + help='Import closed data') + parser.add_argument('--keep-external-reference', dest='keep_external_reference', const=True, + action="store_const", default=False, + help='Store external reference of imported data') + + def handle(self, *args, **options): + admin = User.objects.get(username="admin") + + if options.get('token', None): + token = options.get('token') + else: + print("You need a user token") + return + + importer = PivotalImporter(admin, token) + + if options.get('project_id', None): + project_id = options.get('project_id') + else: + print("Select the project to import:") + for project in importer.list_projects(): + print("- {}: {}".format(project['project_id'], project['project_name'])) + project_id = input("Project id: ") + + users_bindings = {} + if options.get('ask_for_users', None): + print("Add the username or email for next pivotal users:") + for user in importer.list_users(project_id): + try: + users_bindings[user['id']] = User.objects.get(Q(email=user['person'].get('email', "not-valid"))) + break + except User.DoesNotExist: + pass + + while True: + username_or_email = input("{}: ".format(user['person']['name'])) + if username_or_email == "": + break + try: + users_bindings[user['id']] = User.objects.get(Q(username=username_or_email) | Q(email=username_or_email)) + break + except User.DoesNotExist: + print("ERROR: Invalid username or email") + + options = { + "template": options.get('template'), + "import_closed_data": options.get("closed_data", False), + "users_bindings": users_bindings, + "keep_external_reference": options.get('keep_external_reference') + } + importer.import_project(project_id, options) diff --git a/taiga/importers/pivotal/api.py b/taiga/importers/pivotal/api.py new file mode 100644 index 00000000..1176f9aa --- /dev/null +++ b/taiga/importers/pivotal/api.py @@ -0,0 +1,143 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2014-2016 Taiga Agile LLC +# 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 . + +from django.utils.translation import ugettext as _ +from django.conf import settings + +from taiga.base.api import viewsets +from taiga.base import response +from taiga.base import exceptions as exc +from taiga.base.decorators import list_route +from taiga.users.models import AuthData, User +from taiga.users.services import get_user_photo_url +from taiga.users.gravatar import get_user_gravatar_id +from taiga.projects.serializers import ProjectSerializer + +from taiga.importers import permissions +from .importer import PivotalImporter +from . import tasks + + +class PivotalImporterViewSet(viewsets.ViewSet): + permission_classes = (permissions.ImporterPermission,) + + @list_route(methods=["POST"]) + def list_users(self, request, *args, **kwargs): + self.check_permissions(request, "list_users", None) + + token = request.DATA.get('token', None) + project_id = request.DATA.get('project', None) + + if not project_id: + raise exc.WrongArguments(_("The project param is needed")) + + importer = PivotalImporter(request.user, token) + users = importer.list_users(project_id) + for user in users: + user['user'] = None + if not user['email']: + continue + + try: + taiga_user = User.objects.get(email=user['email']) + except User.DoesNotExist: + continue + + user['user'] = { + 'id': taiga_user.id, + 'full_name': taiga_user.get_full_name(), + 'gravatar_id': get_user_gravatar_id(taiga_user), + 'photo': get_user_photo_url(taiga_user), + } + return response.Ok(users) + + @list_route(methods=["POST"]) + def list_projects(self, request, *args, **kwargs): + self.check_permissions(request, "list_projects", None) + token = request.DATA.get('token', None) + importer = PivotalImporter(request.user, token) + projects = importer.list_projects() + return response.Ok(projects) + + @list_route(methods=["POST"]) + def import_project(self, request, *args, **kwargs): + self.check_permissions(request, "import_project", None) + + token = request.DATA.get('token', None) + project_id = request.DATA.get('project', None) + if not project_id: + raise exc.WrongArguments(_("The project param is needed")) + + options = { + "template": request.DATA.get('template', "kanban"), + "users_bindings": request.DATA.get("users_bindings", {}), + "keep_external_reference": request.DATA.get("keep_external_reference", False), + "is_private": request.DATA.get("is_private", False), + } + + if settings.CELERY_ENABLED: + task = tasks.import_project.delay(request.user.id, token, project_id, options) + return response.Accepted({"pivotal_import_id": task.id}) + + importer = PivotalImporter(request.user, token) + project = importer.import_project(project_id, options) + project_data = { + "slug": project.slug, + "my_permissions": ["view_us"], + "is_backlog_activated": project.is_backlog_activated, + "is_kanban_activated": project.is_kanban_activated, + } + + return response.Ok(project_data) + + @list_route(methods=["GET"]) + def auth_url(self, request, *args, **kwargs): + self.check_permissions(request, "auth_url", None) + + (oauth_token, oauth_secret, url) = PivotalImporter.get_auth_url() + + (auth_data, created) = AuthData.objects.get_or_create( + user=request.user, + key="pivotal-oauth", + defaults={ + "value": "", + "extra": {}, + } + ) + auth_data.extra = { + "oauth_token": oauth_token, + "oauth_secret": oauth_secret, + } + auth_data.save() + + return response.Ok({"url": url}) + + @list_route(methods=["POST"]) + def authorize(self, request, *args, **kwargs): + self.check_permissions(request, "authorize", None) + + try: + oauth_data = request.user.auth_data.get(key="pivotal-oauth") + oauth_token = oauth_data.extra['oauth_token'] + oauth_secret = oauth_data.extra['oauth_secret'] + oauth_verifier = request.DATA.get('code') + oauth_data.delete() + pivotal_token = PivotalImporter.get_access_token(oauth_token, oauth_secret, oauth_verifier)['oauth_token'] + except Exception as e: + raise exc.WrongArguments(_("Invalid or expired auth token")) + + return response.Ok({ + "token": pivotal_token + }) diff --git a/taiga/importers/pivotal/importer.py b/taiga/importers/pivotal/importer.py new file mode 100644 index 00000000..6f1456ca --- /dev/null +++ b/taiga/importers/pivotal/importer.py @@ -0,0 +1,702 @@ +from django.core.files.base import ContentFile +from django.contrib.contenttypes.models import ContentType +import requests + +from taiga.users.models import User +from taiga.projects.references.models import recalc_reference_counter +from taiga.projects.models import Project, ProjectTemplate, Membership, Points +from taiga.projects.userstories.models import UserStory, RolePoints +from taiga.projects.tasks.models import Task +from taiga.projects.milestones.models import Milestone +from taiga.projects.epics.models import Epic, RelatedUserStory +from taiga.projects.attachments.models import Attachment +from taiga.projects.history.services import take_snapshot +from taiga.projects.history.services import (make_diff_from_dicts, + make_diff_values, + make_key_from_model_object, + get_typename_for_model_class, + FrozenDiff) +from taiga.projects.history.models import HistoryEntry +from taiga.projects.history.choices import HistoryType +from taiga.projects.custom_attributes.models import UserStoryCustomAttribute +from taiga.mdrender.service import render as mdrender +from taiga.timeline.rebuilder import rebuild_timeline +from taiga.timeline.models import Timeline + + +class PivotalClient: + def __init__(self, token): + self.api_url = "https://www.pivotaltracker.com/services/v5/{}" + self.token = token + self.me = self.get('/me') + + def get(self, uri_path, query_params=None): + headers = { + 'X-TrackerToken': self.token + } + if query_params is None: + query_params = {} + + if uri_path[0] == '/': + uri_path = uri_path[1:] + url = self.api_url.format(uri_path) + + response = requests.get(url, params=query_params, headers=headers) + + if response.status_code == 401: + raise Exception("Unauthorized: %s at %s" % (response.text, url), response) + if response.status_code != 200: + raise Exception("Resource Unavailable: %s at %s" % (response.text, url), response) + + return response.json() + + def get_attachment(self, attachment_id): + headers = { + 'X-TrackerToken': self.token + } + url = "https://www.pivotaltracker.com/file_attachments/{}/download".format(attachment_id) + response = requests.get(url, headers=headers) + return response.content + + +class PivotalImporter: + def __init__(self, user, token): + self._user = user + self._client = PivotalClient(token=token) + + def list_projects(self): + return self._client.me['projects'] + + def list_users(self, project_id): + return self._client.get("/projects/{}/memberships".format(project_id)) + + def import_project(self, project_id, options={"template": "scrum", "users_bindings": {}, "keep_external_reference": False}): + (project, project_data) = self._import_project_data(project_id, options) + self._import_epics_data(project_data, project, options) + self._import_user_stories_data(project_data, project, options) + Timeline.objects.filter(project=project).delete() + rebuild_timeline(None, None, project.id) + recalc_reference_counter(project) + + def _import_project_data(self, project_id, options): + project_data = self._client.get( + "/projects/{}".format(project_id), + { + "fields": ",".join([ + "point_scale", + "name", + "description", + "labels(name)", + ]) + } + ) + project_data['iterations'] = self._client.get( + "/projects/{}/iterations".format(project_id), + { + "fields": ",".join([ + "number", + "start", + "finish", + "stories", + ]) + } + ) + project_data['epics'] = self._client.get( + "/projects/{}/epics".format(project_data['id']), + { + "fields": ",".join([ + "name", + "label", + "description", + "comments(text,file_attachments,google_attachments,person,created_at)", + "follower_ids", + "created_at", + "updated_at", + "url", + ]) + } + ) + + project_template = ProjectTemplate.objects.get(slug=options['template']) + project_template.is_epics_activated = True + project_template.us_statuses = [] + project_template.points = [{ + "value": None, + "name": "?", + "order": 1, + }] + + counter = 2 + for points in project_data['point_scale'].split(","): + project_template.points.append({ + "value": int(points), + "name": points, + "order": counter + }) + counter += 1 + + project_template.us_statuses.append({ + "name": "Unscheduled", + "slug": "unscheduled", + "is_closed": True, + "is_archived": False, + "color": "#999999", + "wip_limit": None, + "order": 1, + }) + project_template.us_statuses.append({ + "name": "Unstarted", + "slug": "unstarted", + "is_closed": False, + "is_archived": False, + "color": "#999999", + "wip_limit": None, + "order": 2, + }) + project_template.us_statuses.append({ + "name": "Planned", + "slug": "planned", + "is_closed": False, + "is_archived": False, + "color": "#999999", + "wip_limit": None, + "order": 3, + }) + project_template.us_statuses.append({ + "name": "Started", + "slug": "started", + "is_closed": False, + "is_archived": False, + "color": "#999999", + "wip_limit": None, + "order": 4, + }) + project_template.us_statuses.append({ + "name": "Finished", + "slug": "finished", + "is_closed": False, + "is_archived": False, + "color": "#999999", + "wip_limit": None, + "order": 5, + }) + project_template.us_statuses.append({ + "name": "Delivered", + "slug": "delivered", + "is_closed": True, + "is_archived": False, + "color": "#999999", + "wip_limit": None, + "order": 6, + }) + project_template.us_statuses.append({ + "name": "Rejected", + "slug": "rejected", + "is_closed": True, + "is_archived": True, + "color": "#999999", + "wip_limit": None, + "order": 7, + }) + project_template.us_statuses.append({ + "name": "Accepted", + "slug": "accepted", + "is_closed": True, + "is_archived": True, + "color": "#999999", + "wip_limit": None, + "order": 8, + }) + project_template.default_options["us_status"] = "Unscheduled" + + project_template.task_statuses = [] + project_template.task_statuses.append({ + "name": "Incomplete", + "slug": "incomplete", + "is_closed": False, + "color": "#ff8a84", + "order": 1, + }) + project_template.task_statuses.append({ + "name": "Complete", + "slug": "complete", + "is_closed": True, + "color": "#669900", + "order": 2, + }) + project_template.default_options["task_status"] = "Incomplete" + + main_permissions = project_template.roles[0]['permissions'] + project_template.roles = [{ + "name": "Main", + "slug": "main", + "computable": True, + "permissions": main_permissions, + "order": 70, + }] + + tags_colors = [] + for label in project_data['labels']: + name = label['name'].lower() + tags_colors.append([name, None]) + + project = Project.objects.create( + name=project_data['name'], + description=project_data.get('description', ''), + owner=self._user, + tags_colors=tags_colors, + creation_template=project_template + ) + + UserStoryCustomAttribute.objects.create( + name="Due date", + description="Due date", + type="date", + order=1, + project=project + ) + UserStoryCustomAttribute.objects.create( + name="Type", + description="Story type", + type="text", + order=2, + project=project + ) + for user in options.get('users_bindings', {}).values(): + if user != self._user: + Membership.objects.get_or_create( + user=user, + project=project, + role=project.get_roles().get(slug="main"), + is_admin=False, + ) + + for iteration in project_data['iterations']: + milestone = Milestone.objects.create( + name="Sprint {}".format(iteration['number']), + slug="sprint-{}".format(iteration['number']), + owner=self._user, + project=project, + estimated_start=iteration['start'][:10], + estimated_finish=iteration['finish'][:10], + ) + Milestone.objects.filter(id=milestone.id).update( + created_date=iteration['start'], + modified_date=iteration['start'], + ) + return (project, project_data) + + def _import_user_stories_data(self, project_data, project, options): + users_bindings = options.get('users_bindings', {}) + epics = {e['label']['id']: e for e in project_data['epics']} + due_date_field = project.userstorycustomattributes.get(name="Due date") + story_type_field = project.userstorycustomattributes.get(name="Type") + story_milestone_binding = {} + for iteration in project_data['iterations']: + for story in iteration['stories']: + story_milestone_binding[story['id']] = Milestone.objects.get( + project=project, + slug="sprint-{}".format(iteration['number']) + ) + + counter = 0 + offset = 0 + while True: + stories = self._client.get("/projects/{}/stories".format(project_data['id']), { + "envelope": "true", + "limit": 300, + "offset": offset, + "fields": ",".join([ + "name", + "description", + "estimate", + "story_type", + "current_state", + "deadline", + "requested_by_id", + "owner_ids", + "labels(id,name)", + "comments(text,file_attachments,google_attachments,person,created_at)", + "tasks(id,description,position,complete,created_at,updated_at)", + "follower_ids", + "created_at", + "updated_at", + "url", + ])}) + offset += 300 + for story in stories['data']: + tags = [] + for label in story['labels']: + tags.append(label['name']) + + assigned_to = None + if len(story['owner_ids']) > 0: + assigned_to = users_bindings.get(story['owner_ids'][0], None) + + owner = users_bindings.get(story['requested_by_id'], self._user) + + external_reference = None + if options.get('keep_external_reference', False): + external_reference = ["pivotal", story['url']] + + us = UserStory.objects.create( + project=project, + owner=owner, + assigned_to=assigned_to, + status=project.us_statuses.get(slug=story['current_state']), + kanban_order=counter, + sprint_order=counter, + backlog_order=counter, + subject=story['name'], + description=story.get('description', ''), + tags=tags, + external_reference=external_reference, + milestone=story_milestone_binding.get(story['id'], None) + ) + + points = Points.objects.get(project=project, value=story.get('estimate', None)) + RolePoints.objects.filter(user_story=us, role__slug="main").update(points_id=points.id) + + if len(story['owner_ids']) > 1: + watchers = list(set(story['owner_ids'][1:] + story['follower_ids'])) + else: + watchers = story['follower_ids'] + + for watcher in watchers: + watcher_user = users_bindings.get(watcher, None) + if watcher_user: + us.add_watcher(watcher_user) + + if story.get('deadline', None): + us.custom_attributes_values.attributes_values = {due_date_field.id: story['deadline']} + us.custom_attributes_values.save() + if story.get('story_type', None): + us.custom_attributes_values.attributes_values = {story_type_field.id: story['story_type']} + us.custom_attributes_values.save() + + UserStory.objects.filter(id=us.id).update( + ref=story['id'], + modified_date=story['updated_at'], + created_date=story['created_at'] + ) + take_snapshot(us, comment="", user=None, delete=False) + + for label in story['labels']: + if epics.get(label['id'], None): + RelatedUserStory.objects.create( + epic=Epic.objects.get(project=project, ref=epics.get(label['id'])['id']), + user_story=us, + order=us.backlog_order + ) + self._import_tasks(project_data, us, story) + self._import_user_story_activity(project_data, us, story, options) + self._import_comments(project_data, us, story, options) + counter += 1 + + if len(stories['data']) < 300: + break + + def _import_epics_data(self, project_data, project, options): + users_bindings = options.get('users_bindings', {}) + counter = 0 + + for epic in project_data['epics']: + external_reference = None + if options.get('keep_external_reference', False): + external_reference = ["pivotal", epic['url']] + + taiga_epic = Epic.objects.create( + project=project, + owner=self._user, + status=project.epic_statuses.get(slug="new"), + epics_order=counter, + subject=epic['name'], + description=epic.get('description', ''), + tags=[], + external_reference=external_reference + ) + + Epic.objects.filter(id=taiga_epic.id).update( + ref=epic['id'], + modified_date=epic['updated_at'], + created_date=epic['created_at'] + ) + + for watcher in epic['follower_ids']: + watcher_user = users_bindings.get(watcher, None) + if watcher_user: + taiga_epic.add_watcher(watcher_user) + + take_snapshot(taiga_epic, comment="", user=None, delete=False) + self._import_comments(project_data, taiga_epic, epic, options) + self._import_epic_activity(project_data, taiga_epic, epic, options) + counter += 1 + + def _import_tasks(self, project_data, us, story): + for task in story['tasks']: + taiga_task = Task.objects.create( + subject=task['description'], + status=us.project.task_statuses.get(slug="complete" if task['complete'] else "incomplete"), + project=us.project, + us_order=task['position'], + taskboard_order=task['position'], + user_story=us + ) + + Task.objects.filter(id=taiga_task.id).update( + ref=task['id'], + modified_date=task['updated_at'], + created_date=task['created_at'] + ) + take_snapshot(taiga_task, comment="", user=None, delete=False) + + def _import_attachment(self, obj, attachment_id, attachment_name, created_at, person_id, options): + users_bindings = options.get('users_bindings', {}) + + data = self._client.get_attachment(attachment_id) + att = Attachment( + owner=users_bindings.get(person_id, self._user), + project=obj.project, + content_type=ContentType.objects.get_for_model(obj), + object_id=obj.id, + name=attachment_name, + size=len(data), + created_date=created_at, + is_deprecated=False, + ) + att.attached_file.save(attachment_name, ContentFile(data), save=True) + + def _import_comments(self, project_data, obj, story, options): + users_bindings = options.get('users_bindings', {}) + + for comment in story['comments']: + if 'text' in comment: + snapshot = take_snapshot( + obj, + comment=comment['text'], + user=users_bindings.get(comment['person']['id'], User(full_name=comment['person']['name'])), + delete=False + ) + HistoryEntry.objects.filter(id=snapshot.id).update(created_at=comment['created_at']) + for attachment in comment['file_attachments']: + self._import_attachment( + obj, + attachment['id'], + attachment['filename'], + comment['created_at'], + comment['person']['id'], + options + ) + + def _import_user_story_activity(self, project_data, us, story, options): + offset = 0 + while True: + activities = self._client.get( + "/projects/{}/stories/{}/activity".format( + project_data['id'], + story['id'], + ), + {"envelope": "true", "limit": 300, "offset": offset} + ) + offset += 300 + for activity in activities['data']: + self._import_activity(us, activity, options) + + if len(activities['data']) < 300: + break + + def _import_epic_activity(self, project_data, taiga_epic, epic, options): + offset = 0 + while True: + activities = self._client.get( + "/projects/{}/epics/{}/activity".format( + project_data['id'], + epic['id'], + ), + {"envelope": "true", "limit": 300, "offset": offset} + ) + offset += 300 + for activity in activities['data']: + self._import_activity(taiga_epic, activity, options) + + if len(activities['data']) < 300: + break + + def _import_activity(self, obj, activity, options): + activity_data = self._transform_activity_data(obj, activity, options) + if activity_data is None: + return + + change_old = activity_data['change_old'] + change_new = activity_data['change_new'] + hist_type = activity_data['hist_type'] + comment = activity_data['comment'] + user = activity_data['user'] + + key = make_key_from_model_object(activity_data['obj']) + typename = get_typename_for_model_class(type(activity_data['obj'])) + + diff = make_diff_from_dicts(change_old, change_new) + fdiff = FrozenDiff(key, diff, {}) + + entry = HistoryEntry.objects.create( + user=user, + project_id=obj.project.id, + key=key, + type=hist_type, + snapshot=None, + diff=fdiff.diff, + values=make_diff_values(typename, fdiff), + comment=comment, + comment_html=mdrender(obj.project, comment), + is_hidden=False, + is_snapshot=False, + ) + HistoryEntry.objects.filter(id=entry.id).update(created_at=activity['occurred_at']) + return HistoryEntry.objects.get(id=entry.id) + + def _transform_activity_data(self, obj, activity, options): + users_bindings = options.get('users_bindings', {}) + due_date_field = obj.project.userstorycustomattributes.get(name="Due date") + story_type_field = obj.project.userstorycustomattributes.get(name="Type") + + user = {"pk": None, "name": activity.get('performed_by', {}).get('name', None)} + taiga_user = users_bindings.get(activity.get('performed_by', {}).get('id', None), None) + if taiga_user: + user = {"pk": taiga_user.id, "name": taiga_user.get_full_name()} + + result = { + "change_old": {}, + "change_new": {}, + "hist_type": HistoryType.change, + "comment": "", + "user": user, + "obj": obj + } + + if activity['kind'] == "story_create_activity": + UserStory.objects.filter(id=obj.id, created_date__gt=activity['occurred_at']).update( + created_date=activity['occurred_at'], + owner=users_bindings.get(activity["performed_by"]["id"], self._user) + ) + return None + elif activity['kind'] == "epic_create_activity": + Epic.objects.filter(id=obj.id, created_date__gt=activity['occurred_at']).update( + created_date=activity['occurred_at'], + owner=users_bindings.get(activity["performed_by"]["id"], self._user) + ) + return None + elif activity['kind'] in ["story_update_activity", "epic_update_activity"]: + for change in activity['changes']: + if change['change_type'] != "update" or change['kind'] not in ["story", "epic"]: + continue + + if 'description' in change['new_values']: + result['change_old']["description"] = str(change['original_values']['description']) + result['change_new']["description"] = str(change['new_values']['description']) + result['change_old']["description_html"] = mdrender(obj.project, str(change['original_values']['description'])) + result['change_new']["description_html"] = mdrender(obj.project, str(change['new_values']['description'])) + + if 'estimate' in change['new_values']: + old_points = None + if change['original_values']['estimate']: + estimation = change['original_values']['estimate'] + (old_points, _) = Points.objects.get_or_create( + project=obj.project, + value=estimation, + defaults={ + "name": str(estimation), + "order": estimation, + } + ) + old_points = old_points.id + new_points = None + if change['new_values']['estimate']: + estimation = change['new_values']['estimate'] + (new_points, _) = Points.objects.get_or_create( + project=obj.project, + value=estimation, + defaults={ + "name": str(estimation), + "order": estimation, + } + ) + new_points = new_points.id + result['change_old']["points"] = {obj.project.roles.get(slug="main").id: old_points} + result['change_new']["points"] = {obj.project.roles.get(slug="main").id: new_points} + + if 'name' in change['new_values']: + result['change_old']["subject"] = change['original_values']['name'] + result['change_new']["subject"] = change['new_values']['name'] + + if 'labels' in change['new_values']: + result['change_old']["tags"] = [l.lower() for l in change['original_values']['labels']] + result['change_new']["tags"] = [l.lower() for l in change['new_values']['labels']] + + if 'current_state' in change['new_values']: + result['change_old']["status"] = obj.project.us_statuses.get(slug=change['original_values']['current_state']).id + result['change_new']["status"] = obj.project.us_statuses.get(slug=change['new_values']['current_state']).id + + if 'story_type' in change['new_values']: + if "custom_attributes" not in result['change_old']: + result['change_old']["custom_attributes"] = [] + if "custom_attributes" not in result['change_new']: + result['change_new']["custom_attributes"] = [] + + result['change_old']["custom_attributes"].append({ + "name": "Type", + "value": change['original_values']['story_type'], + "id": story_type_field.id + }) + result['change_new']["custom_attributes"].append({ + "name": "Type", + "value": change['new_values']['story_type'], + "id": story_type_field.id + }) + + if 'deadline' in change['new_values']: + if "custom_attributes" not in result['change_old']: + result['change_old']["custom_attributes"] = [] + if "custom_attributes" not in result['change_new']: + result['change_new']["custom_attributes"] = [] + + result['change_old']["custom_attributes"].append({ + "name": "Due date", + "value": change['original_values']['deadline'], + "id": due_date_field.id + }) + result['change_new']["custom_attributes"].append({ + "name": "Due date", + "value": change['new_values']['deadline'], + "id": due_date_field.id + }) + + # TODO: Process owners_ids + + elif activity['kind'] == "task_create_activity": + return None + elif activity['kind'] == "task_update_activity": + for change in activity['changes']: + if change['change_type'] != "update" or change['kind'] != "task": + continue + + try: + task = Task.objects.get(project=obj.project, ref=change['id']) + if 'description' in change['new_values']: + result['change_old']["subject"] = change['original_values']['description'] + result['change_new']["subject"] = change['new_values']['description'] + result['obj'] = task + if 'complete' in change['new_values']: + result['change_old']["status"] = obj.project.task_statuses.get(slug="complete" if change['original_values']['complete'] else "incomplete").id + result['change_new']["status"] = obj.project.task_statuses.get(slug="complete" if change['new_values']['complete'] else "incomplete").id + result['obj'] = task + except Task.DoesNotExist: + return None + + elif activity['kind'] == "comment_create_activity": + return None + elif activity['kind'] == "comment_update_activity": + return None + elif activity['kind'] == "story_move_activity": + return None + return result diff --git a/taiga/importers/pivotal/tasks.py b/taiga/importers/pivotal/tasks.py new file mode 100644 index 00000000..2e8f54ea --- /dev/null +++ b/taiga/importers/pivotal/tasks.py @@ -0,0 +1,56 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino +# Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso +# 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 . + +import logging +import sys + +from django.utils.translation import ugettext as _ + +from taiga.base.mails import mail_builder +from taiga.users.models import User +from taiga.celery import app +from .importer import PivotalImporter + +logger = logging.getLogger('taiga.importers.pivotal') + + +@app.task(bind=True) +def import_project(self, user_id, token, project_id, options): + user = User.object.get(id=user_id) + importer = PivotalImporter(user, token) + try: + project = importer.import_project(project_id, options) + except Exception as e: + # Error + ctx = { + "user": user, + "error_subject": _("Error importing pivotal project"), + "error_message": _("Error importing pivotal project"), + "project": project_id, + "exception": e + } + email = mail_builder.pivotal_import_error(admin, ctx) + email.send() + logger.error('Error importing pivotal project %s (by %s)', project_id, user, exc_info=sys.exc_info()) + else: + ctx = { + "project": project, + "user": user, + } + email = mail_builder.pivotal_import_success(user, ctx) + email.send()