Add jira importer

remotes/origin/github-import
Jesús Espino 2017-01-18 09:39:10 +01:00 committed by David Barragán Merino
parent f7595b65cc
commit 723034d373
9 changed files with 2245 additions and 0 deletions

View File

@ -565,6 +565,10 @@ from .sr import *
TRELLO_API_KEY = ""
TRELLO_SECRET_KEY = ""
JIRA_CONSUMER_KEY = ""
JIRA_CERT = ""
JIRA_PUB_CERT = ""
# NOTE: DON'T INSERT MORE SETTINGS AFTER THIS LINE
TEST_RUNNER="django.test.runner.DiscoverRunner"

View File

@ -0,0 +1,357 @@
import datetime
from django.template.defaultfilters import slugify
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.history.services import take_snapshot
from taiga.timeline.rebuilder import rebuild_timeline
from taiga.timeline.models import Timeline
from .common import JiraImporterCommon
class JiraAgileImporter(JiraImporterCommon):
def list_projects(self):
return [{"id": board['id'],
"name": board['name'],
"description": "",
"is_private": True,
"importer_type": "agile"} for board in self._client.get_agile('/board')['values']]
def list_issue_types(self, project_id):
board_project = self._client.get_agile("/board/{}/project".format(project_id))['values'][0]
statuses = self._client.get("/project/{}/statuses".format(board_project['id']))
return statuses
def import_project(self, project_id, options=None):
self.resolve_user_bindings(options)
project = self._import_project_data(project_id, options)
self._import_epics_data(project_id, project, options)
self._import_user_stories_data(project_id, project, options)
self._cleanup(project, options)
Timeline.objects.filter(project=project).delete()
rebuild_timeline(None, None, project.id)
recalc_reference_counter(project)
return project
def _import_project_data(self, project_id, options):
project = self._client.get_agile("/board/{}".format(project_id))
project_config = self._client.get_agile("/board/{}/configuration".format(project_id))
if project['type'] == "scrum":
project_template = ProjectTemplate.objects.get(slug="scrum")
options['type'] = "scrum"
elif project['type'] == "kanban":
project_template = ProjectTemplate.objects.get(slug="kanban")
options['type'] = "kanban"
project_template.is_epics_activated = True
project_template.epic_statuses = []
project_template.us_statuses = []
project_template.task_statuses = []
project_template.issue_statuses = []
counter = 0
for column in project_config['columnConfig']['columns']:
project_template.epic_statuses.append({
"name": column['name'],
"slug": slugify(column['name']),
"is_closed": False,
"is_archived": False,
"color": "#999999",
"wip_limit": None,
"order": counter,
})
project_template.us_statuses.append({
"name": column['name'],
"slug": slugify(column['name']),
"is_closed": False,
"is_archived": False,
"color": "#999999",
"wip_limit": None,
"order": counter,
})
project_template.task_statuses.append({
"name": column['name'],
"slug": slugify(column['name']),
"is_closed": False,
"is_archived": False,
"color": "#999999",
"wip_limit": None,
"order": counter,
})
project_template.issue_statuses.append({
"name": column['name'],
"slug": slugify(column['name']),
"is_closed": False,
"is_archived": False,
"color": "#999999",
"wip_limit": None,
"order": counter,
})
counter += 1
project_template.default_options["epic_status"] = project_template.epic_statuses[0]['name']
project_template.default_options["us_status"] = project_template.us_statuses[0]['name']
project_template.default_options["task_status"] = project_template.task_statuses[0]['name']
project_template.default_options["issue_status"] = project_template.issue_statuses[0]['name']
project_template.points = [{
"value": None,
"name": "?",
"order": 0,
}]
main_permissions = project_template.roles[0]['permissions']
project_template.roles = [{
"name": "Main",
"slug": "main",
"computable": True,
"permissions": main_permissions,
"order": 70,
}]
project = Project.objects.create(
name=options.get('name', None) or project['name'],
description=options.get('description', None) or project.get('description', ''),
owner=self._user,
creation_template=project_template,
is_private=options.get('is_private', False),
)
self._create_custom_fields(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,
)
if project_template.slug == "scrum":
for sprint in self._client.get_agile("/board/{}/sprint".format(project_id))['values']:
start_datetime = sprint.get('startDate', None)
end_datetime = sprint.get('startDate', None)
start_date = datetime.date.today()
if start_datetime:
start_date = start_datetime[:10]
end_date = datetime.date.today()
if end_datetime:
end_date = end_datetime[:10]
milestone = Milestone.objects.create(
name=sprint['name'],
slug=slugify(sprint['name']),
owner=self._user,
project=project,
estimated_start=start_date,
estimated_finish=end_date,
)
Milestone.objects.filter(id=milestone.id).update(
created_date=start_datetime or datetime.datetime.now(),
modified_date=start_datetime or datetime.datetime.now(),
)
return project
def _import_user_stories_data(self, project_id, project, options):
users_bindings = options.get('users_bindings', {})
project_conf = self._client.get_agile("/board/{}/configuration".format(project_id))
if options['type'] == "scrum":
estimation_field = project_conf['estimation']['field']['fieldId']
counter = 0
offset = 0
while True:
issues = self._client.get_agile("/board/{}/issue".format(project_id), {
"startAt": offset,
"expand": "changelog",
})
offset += issues['maxResults']
for issue in issues['issues']:
issue['fields']['issuelinks'] += self._client.get("/issue/{}/remotelink".format(issue['key']))
assigned_to = users_bindings.get(issue['fields']['assignee']['key'] if issue['fields']['assignee'] else None, None)
owner = users_bindings.get(issue['fields']['creator']['key'] if issue['fields']['creator'] else None, self._user)
external_reference = None
if options.get('keep_external_reference', False):
external_reference = ["jira", self._client.get_issue_url(issue['key'])]
try:
milestone = project.milestones.get(name=issue['fields'].get('sprint', {}).get('name', ''))
except Milestone.DoesNotExist:
milestone = None
us = UserStory.objects.create(
project=project,
owner=owner,
assigned_to=assigned_to,
status=project.us_statuses.get(name=issue['fields']['status']['name']),
kanban_order=counter,
sprint_order=counter,
backlog_order=counter,
subject=issue['fields']['summary'],
description=issue['fields']['description'] or '',
tags=issue['fields']['labels'],
external_reference=external_reference,
milestone=milestone,
)
try:
epic = project.epics.get(ref=int(issue['fields'].get("epic", {}).get("key", "FAKE-0").split("-")[1]))
RelatedUserStory.objects.create(
user_story=us,
epic=epic,
order=1
)
except Epic.DoesNotExist:
pass
if options['type'] == "scrum":
estimation = None
if issue['fields'].get(estimation_field, None):
estimation = float(issue['fields'].get(estimation_field))
(points, _) = Points.objects.get_or_create(
project=project,
value=estimation,
defaults={
"name": str(estimation),
"order": estimation,
}
)
RolePoints.objects.filter(user_story=us, role__slug="main").update(points_id=points.id)
self._import_to_custom_fields(us, issue, options)
us.ref = issue['key'].split("-")[1]
UserStory.objects.filter(id=us.id).update(
ref=us.ref,
modified_date=issue['fields']['updated'],
created_date=issue['fields']['created']
)
take_snapshot(us, comment="", user=None, delete=False)
self._import_subtasks(project_id, project, us, issue, options)
self._import_comments(us, issue, options)
self._import_attachments(us, issue, options)
self._import_changelog(project, us, issue, options)
counter += 1
if len(issues['issues']) < issues['maxResults']:
break
def _import_subtasks(self, project_id, project, us, issue, options):
users_bindings = options.get('users_bindings', {})
if len(issue['fields']['subtasks']) == 0:
return
counter = 0
offset = 0
while True:
issues = self._client.get_agile("/board/{}/issue".format(project_id), {
"jql": "parent={}".format(issue['key']),
"startAt": offset,
"expand": "changelog",
})
offset += issues['maxResults']
for issue in issues['issues']:
issue['fields']['issuelinks'] += self._client.get("/issue/{}/remotelink".format(issue['key']))
assigned_to = users_bindings.get(issue['fields']['assignee']['key'] if issue['fields']['assignee'] else None, None)
owner = users_bindings.get(issue['fields']['creator']['key'] if issue['fields']['creator'] else None, self._user)
external_reference = None
if options.get('keep_external_reference', False):
external_reference = ["jira", self._client.get_issue_url(issue['key'])]
task = Task.objects.create(
user_story=us,
project=project,
owner=owner,
assigned_to=assigned_to,
status=project.task_statuses.get(name=issue['fields']['status']['name']),
subject=issue['fields']['summary'],
description=issue['fields']['description'] or '',
tags=issue['fields']['labels'],
external_reference=external_reference,
milestone=us.milestone,
)
self._import_to_custom_fields(task, issue, options)
task.ref = issue['key'].split("-")[1]
Task.objects.filter(id=task.id).update(
ref=task.ref,
modified_date=issue['fields']['updated'],
created_date=issue['fields']['created']
)
take_snapshot(task, comment="", user=None, delete=False)
for subtask in issue['fields']['subtasks']:
print("WARNING: Ignoring subtask {} because parent isn't a User Story".format(subtask['key']))
self._import_comments(task, issue, options)
self._import_attachments(task, issue, options)
self._import_changelog(project, task, issue, options)
counter += 1
if len(issues['issues']) < issues['maxResults']:
break
def _import_epics_data(self, project_id, project, options):
users_bindings = options.get('users_bindings', {})
counter = 0
offset = 0
while True:
issues = self._client.get_agile("/board/{}/epic".format(project_id), {
"startAt": offset,
})
offset += issues['maxResults']
for epic in issues['values']:
issue = self._client.get_agile("/issue/{}".format(epic['key']))
issue['fields']['issuelinks'] += self._client.get("/issue/{}/remotelink".format(issue['key']))
assigned_to = users_bindings.get(issue['fields']['assignee']['key'] if issue['fields']['assignee'] else None, None)
owner = users_bindings.get(issue['fields']['creator']['key'] if issue['fields']['creator'] else None, self._user)
external_reference = None
if options.get('keep_external_reference', False):
external_reference = ["jira", self._client.get_issue_url(issue['key'])]
epic = Epic.objects.create(
project=project,
owner=owner,
assigned_to=assigned_to,
status=project.epic_statuses.get(name=issue['fields']['status']['name']),
subject=issue['fields']['summary'],
description=issue['fields']['description'] or '',
epics_order=counter,
tags=issue['fields']['labels'],
external_reference=external_reference,
)
self._import_to_custom_fields(epic, issue, options)
epic.ref = issue['key'].split("-")[1]
Epic.objects.filter(id=epic.id).update(
ref=epic.ref,
modified_date=issue['fields']['updated'],
created_date=issue['fields']['created']
)
take_snapshot(epic, comment="", user=None, delete=False)
for subtask in issue['fields']['subtasks']:
print("WARNING: Ignoring subtask {} because parent isn't a User Story".format(subtask['key']))
self._import_comments(epic, issue, options)
self._import_attachments(epic, issue, options)
issue_with_changelog = self._client.get("/issue/{}".format(issue['key']), {
"expand": "changelog"
})
self._import_changelog(project, epic, issue_with_changelog, options)
counter += 1
if len(issues['values']) < issues['maxResults']:
break

229
taiga/importers/jira/api.py Normal file
View File

@ -0,0 +1,229 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2014-2016 Taiga Agile LLC <support@taiga.io>
# 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 <http://www.gnu.org/licenses/>.
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.importers import permissions
from .normal import JiraNormalImporter
from .agile import JiraAgileImporter
from . import tasks
class JiraImporterViewSet(viewsets.ViewSet):
permission_classes = (permissions.ImporterPermission,)
def _get_token(self, request):
token_data = request.DATA.get('token', "").split(".")
token = {
"access_token": token_data[0],
"access_token_secret": token_data[1],
"key_cert": settings.JIRA_CERT,
"consumer_key": settings.JIRA_CONSUMER_KEY
}
return token
@list_route(methods=["POST"])
def list_users(self, request, *args, **kwargs):
self.check_permissions(request, "list_users", None)
url = request.DATA.get('url', None)
token = self._get_token(request)
project_id = request.DATA.get('project', None)
if not project_id:
raise exc.WrongArguments(_("The project param is needed"))
if not url:
raise exc.WrongArguments(_("The url param is needed"))
importer = JiraNormalImporter(request.user, url, token)
users = importer.list_users()
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)
url = request.DATA.get('url', None)
if not url:
raise exc.WrongArguments(_("The url param is needed"))
token = self._get_token(request)
importer = JiraNormalImporter(request.user, url, token)
agile_importer = JiraAgileImporter(request.user, url, token)
projects = importer.list_projects()
boards = agile_importer.list_projects()
return response.Ok(sorted(projects + boards, key=lambda x: x['name']))
@list_route(methods=["POST"])
def import_project(self, request, *args, **kwargs):
self.check_permissions(request, "import_project", None)
url = request.DATA.get('url', None)
token = self._get_token(request)
project_id = request.DATA.get('project', None)
if not project_id:
raise exc.WrongArguments(_("The project param is needed"))
if not url:
raise exc.WrongArguments(_("The url param is needed"))
options = {
"name": request.DATA.get('name', None),
"description": request.DATA.get('description', None),
"users_bindings": request.DATA.get("user_bindings", {}),
"keep_external_reference": request.DATA.get("keep_external_reference", False),
"is_private": request.DATA.get("is_private", False),
}
importer_type = request.DATA.get('importer_type', "normal")
if importer_type == "agile":
importer = JiraAgileImporter(request.user, url, token)
else:
project_type = request.DATA.get("project_type", "scrum")
if project_type == "kanban":
options['template'] = "kanban"
else:
options['template'] = "scrum"
importer = JiraNormalImporter(request.user, url, token)
types_bindings = {
"epic": [],
"us": [],
"task": [],
"issue": [],
}
for issue_type in importer.list_issue_types(project_id):
if project_type in ['scrum', 'kanban']:
# Set the type bindings
if issue_type['subtask']:
types_bindings['task'].append(issue_type)
elif issue_type['name'].upper() == "EPIC":
types_bindings["epic"].append(issue_type)
elif issue_type['name'].upper() in ["US", "USERSTORY", "USER STORY"]:
types_bindings["us"].append(issue_type)
elif issue_type['name'].upper() in ["ISSUE", "BUG", "ENHANCEMENT"]:
types_bindings["issue"].append(issue_type)
else:
types_bindings["us"].append(issue_type)
elif project_type == "issues":
# Set the type bindings
if issue_type['subtask']:
continue
types_bindings["issue"].append(issue_type)
elif project_type == "issues-with-subissues":
types_bindings["issue"].append(issue_type)
else:
raise exc.WrongArguments(_("Invalid project_type {}").format(project_type))
options["types_bindings"] = types_bindings
if settings.CELERY_ENABLED:
task = tasks.import_project.delay(request.user.id, url, token, project_id, options, importer_type)
return response.Accepted({"task_id": task.id})
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)
jira_url = request.QUERY_PARAMS.get('url', None)
if not jira_url:
raise exc.WrongArguments(_("The url param is needed"))
(oauth_token, oauth_secret, url) = JiraNormalImporter.get_auth_url(
jira_url,
settings.JIRA_CONSUMER_KEY,
settings.JIRA_CERT,
True
)
(auth_data, created) = AuthData.objects.get_or_create(
user=request.user,
key="jira-oauth",
defaults={
"value": "",
"extra": {},
}
)
auth_data.extra = {
"oauth_token": oauth_token,
"oauth_secret": oauth_secret,
"url": jira_url,
}
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="jira-oauth")
oauth_token = oauth_data.extra['oauth_token']
oauth_secret = oauth_data.extra['oauth_secret']
server_url = oauth_data.extra['url']
oauth_data.delete()
jira_token = JiraNormalImporter.get_access_token(
server_url,
settings.JIRA_CONSUMER_KEY,
settings.JIRA_CERT,
oauth_token,
oauth_secret,
True
)
except Exception as e:
raise exc.WrongArguments(_("Invalid or expired auth token"))
return response.Ok({
"token": jira_token['access_token'] + "." + jira_token['access_token_secret'],
"url": server_url
})

View File

@ -0,0 +1,748 @@
import requests
from urllib.parse import parse_qsl
from oauthlib.oauth1 import SIGNATURE_RSA
from requests_oauthlib import OAuth1
from django.core.files.base import ContentFile
from django.contrib.contenttypes.models import ContentType
from taiga.users.models import User
from taiga.projects.models import Project, ProjectTemplate, Membership, Points
from taiga.projects.userstories.models import UserStory
from taiga.projects.tasks.models import Task
from taiga.projects.issues.models import Issue
from taiga.projects.milestones.models import Milestone
from taiga.projects.epics.models import Epic
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.custom_attributes.models import (UserStoryCustomAttribute,
TaskCustomAttribute,
IssueCustomAttribute,
EpicCustomAttribute)
from taiga.projects.history.models import HistoryEntry
from taiga.projects.history.choices import HistoryType
from taiga.mdrender.service import render as mdrender
EPIC_COLORS = {
"ghx-label-0": "#ffffff",
"ghx-label-1": "#815b3a",
"ghx-label-2": "#f79232",
"ghx-label-3": "#d39c3f",
"ghx-label-4": "#3b7fc4",
"ghx-label-5": "#4a6785",
"ghx-label-6": "#8eb021",
"ghx-label-7": "#ac707a",
"ghx-label-8": "#654982",
"ghx-label-9": "#f15c75",
}
def links_to_richtext(importer, issue, links):
richtext = ""
importing_project_key = issue['key'].split("-")[0]
for link in links:
if "inwardIssue" in link:
(project_key, issue_key) = link['inwardIssue']['key'].split("-")
action = link['type']['inward']
elif "outwardIssue" in link:
(project_key, issue_key) = link['outwardIssue']['key'].split("-")
action = link['type']['outward']
else:
continue
if importing_project_key == project_key:
richtext += " * This item {} #{}\n".format(action, issue_key)
else:
url = importer._client.server + "/projects/{}/issues/{}-{}".format(
project_key,
project_key,
issue_key
)
richtext += " * This item {} [{}-{}]({})\n".format(action, project_key, issue_key, url)
for link in links:
if "object" in link:
richtext += " * [{}]({})\n".format(
link['object']['title'] or link['object']['url'],
link['object']['url'],
)
return richtext
class JiraClient:
def __init__(self, server, oauth):
self.server = server
self.api_url = server + "/rest/agile/1.0/{}"
self.main_api_url = server + "/rest/api/2/{}"
if oauth:
self.oauth = OAuth1(
oauth['consumer_key'],
signature_method=SIGNATURE_RSA,
rsa_key=oauth['key_cert'],
resource_owner_key=oauth['access_token'],
resource_owner_secret=oauth['access_token_secret']
)
else:
self.oauth = None
def get(self, uri_path, query_params=None):
headers = {
'Content-Type': "application/json"
}
if query_params is None:
query_params = {}
if uri_path[0] == '/':
uri_path = uri_path[1:]
url = self.main_api_url.format(uri_path)
response = requests.get(url, params=query_params, headers=headers, auth=self.oauth)
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_agile(self, uri_path, query_params=None):
headers = {
'Content-Type': "application/json"
}
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, auth=self.oauth)
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 raw_get(self, absolute_uri, query_params=None):
if query_params is None:
query_params = {}
response = requests.get(absolute_uri, params=query_params, auth=self.oauth)
if response.status_code == 401:
raise Exception("Unauthorized: %s at %s" % (response.text, absolute_uri), response)
if response.status_code != 200:
raise Exception("Resource Unavailable: %s at %s" % (response.text, absolute_uri), response)
return response.content
def get_issue_url(self, key):
(project_key, issue_key) = key.split("-")
return self.server + "/projects/{}/issues/{}".format(project_key, key)
class JiraImporterCommon:
def __init__(self, user, server, oauth):
self._user = user
self._client = JiraClient(server=server, oauth=oauth)
def resolve_user_bindings(self, options):
for option in list(options['users_bindings'].keys()):
try:
user = User.objects.get(id=options['users_bindings'][option])
options['users_bindings'][option] = user
except User.DoesNotExist:
del(options['users_bindings'][option])
def list_users(self):
result = []
users = self._client.get("/user/picker", {
"query": "@",
"maxResults": 1000,
})
for user in users['users']:
user_data = self._client.get("/user", {
"key": user['key']
})
result.append({
"id": user_data['key'],
"full_name": user_data['displayName'],
"email": user_data['emailAddress'],
})
return result
def _import_comments(self, obj, issue, options):
users_bindings = options.get('users_bindings', {})
offset = 0
while True:
comments = self._client.get("/issue/{}/comment".format(issue['key']), {"startAt": offset})
for comment in comments['comments']:
snapshot = take_snapshot(
obj,
comment=comment['body'],
user=users_bindings.get(
comment['author']['name'],
User(full_name=comment['author']['displayName'])
),
delete=False
)
HistoryEntry.objects.filter(id=snapshot.id).update(created_at=comment['created'])
offset += len(comments['comments'])
if len(comments['comments']) <= comments['maxResults']:
break
def _create_custom_fields(self, project):
custom_fields = []
for model in [UserStoryCustomAttribute, TaskCustomAttribute, IssueCustomAttribute, EpicCustomAttribute]:
model.objects.create(
name="Due date",
description="Due date",
type="date",
order=1,
project=project
)
model.objects.create(
name="Priority",
description="Priority",
type="text",
order=1,
project=project
)
model.objects.create(
name="Resolution",
description="Resolution",
type="text",
order=1,
project=project
)
model.objects.create(
name="Resolution date",
description="Resolution date",
type="date",
order=1,
project=project
)
model.objects.create(
name="Environment",
description="Environment",
type="text",
order=1,
project=project
)
model.objects.create(
name="Components",
description="Components",
type="text",
order=1,
project=project
)
model.objects.create(
name="Affects Version/s",
description="Affects Version/s",
type="text",
order=1,
project=project
)
model.objects.create(
name="Fix Version/s",
description="Fix Version/s",
type="text",
order=1,
project=project
)
model.objects.create(
name="Links",
description="Links",
type="richtext",
order=1,
project=project
)
custom_fields.append({
"history_name": "duedate",
"jira_field_name": "duedate",
"taiga_field_name": "Due date",
})
custom_fields.append({
"history_name": "priority",
"jira_field_name": "priority",
"taiga_field_name": "Priority",
"transform": lambda issue, obj: obj.get('name', None)
})
custom_fields.append({
"history_name": "resolution",
"jira_field_name": "resolution",
"taiga_field_name": "Resolution",
"transform": lambda issue, obj: obj.get('name', None)
})
custom_fields.append({
"history_name": "Resolution date",
"jira_field_name": "resolutiondate",
"taiga_field_name": "Resolution date",
})
custom_fields.append({
"history_name": "environment",
"jira_field_name": "environment",
"taiga_field_name": "Environment",
})
custom_fields.append({
"history_name": "Component",
"jira_field_name": "components",
"taiga_field_name": "Components",
"transform": lambda issue, obj: ", ".join([c.get('name', None) for c in obj])
})
custom_fields.append({
"history_name": "Version",
"jira_field_name": "versions",
"taiga_field_name": "Affects Version/s",
"transform": lambda issue, obj: ", ".join([c.get('name', None) for c in obj])
})
custom_fields.append({
"history_name": "Fix Version",
"jira_field_name": "fixVersions",
"taiga_field_name": "Fix Version/s",
"transform": lambda issue, obj: ", ".join([c.get('name', None) for c in obj])
})
custom_fields.append({
"history_name": "Link",
"jira_field_name": "issuelinks",
"taiga_field_name": "Links",
"transform": lambda issue, obj: links_to_richtext(self, issue, obj)
})
greenhopper_fields = {}
for custom_field in self._client.get("/field"):
if custom_field['custom']:
if custom_field['schema']['custom'] == "com.pyxis.greenhopper.jira:gh-sprint":
greenhopper_fields["sprint"] = custom_field['id']
elif custom_field['schema']['custom'] == "com.pyxis.greenhopper.jira:gh-epic-link":
greenhopper_fields["link"] = custom_field['id']
elif custom_field['schema']['custom'] == "com.pyxis.greenhopper.jira:gh-epic-status":
greenhopper_fields["status"] = custom_field['id']
elif custom_field['schema']['custom'] == "com.pyxis.greenhopper.jira:gh-epic-label":
greenhopper_fields["label"] = custom_field['id']
elif custom_field['schema']['custom'] == "com.pyxis.greenhopper.jira:gh-epic-color":
greenhopper_fields["color"] = custom_field['id']
elif custom_field['schema']['custom'] == "com.pyxis.greenhopper.jira:gh-lexo-rank":
greenhopper_fields["rank"] = custom_field['id']
elif (
custom_field['name'] == "Story Points" and
custom_field['schema']['custom'] == 'com.atlassian.jira.plugin.system.customfieldtypes:float'
):
greenhopper_fields["points"] = custom_field['id']
else:
multiline_types = [
"com.atlassian.jira.plugin.system.customfieldtypes:textarea"
]
date_types = [
"com.atlassian.jira.plugin.system.customfieldtypes:datepicker"
"com.atlassian.jira.plugin.system.customfieldtypes:datetime"
]
if custom_field['schema']['custom'] in multiline_types:
field_type = "multiline"
elif custom_field['schema']['custom'] in date_types:
field_type = "date"
else:
field_type = "text"
custom_field_data = {
"name": custom_field['name'][:64],
"description": custom_field['name'],
"type": field_type,
"order": 1,
"project": project
}
UserStoryCustomAttribute.objects.get_or_create(**custom_field_data)
TaskCustomAttribute.objects.get_or_create(**custom_field_data)
IssueCustomAttribute.objects.get_or_create(**custom_field_data)
EpicCustomAttribute.objects.get_or_create(**custom_field_data)
custom_fields.append({
"history_name": custom_field['name'],
"jira_field_name": custom_field['id'],
"taiga_field_name": custom_field['name'][:64],
})
self.greenhopper_fields = greenhopper_fields
self.custom_fields = custom_fields
def _import_to_custom_fields(self, obj, issue, options):
if isinstance(obj, Epic):
custom_att_manager = obj.project.epiccustomattributes
elif isinstance(obj, UserStory):
custom_att_manager = obj.project.userstorycustomattributes
elif isinstance(obj, Task):
custom_att_manager = obj.project.taskcustomattributes
elif isinstance(obj, Issue):
custom_att_manager = obj.project.issuecustomattributes
else:
raise NotImplementedError("Not implemented custom attributes for this object ({})".format(obj))
custom_attributes_values = {}
for custom_field in self.custom_fields:
if issue['key'] == "PS-10":
import pprint; pprint.pprint(issue['fields'])
data = issue['fields'].get(custom_field['jira_field_name'], None)
if data and "transform" in custom_field:
data = custom_field['transform'](issue, data)
if data:
taiga_field = custom_att_manager.get(name=custom_field['taiga_field_name'])
custom_attributes_values[taiga_field.id] = data
if custom_attributes_values != {}:
obj.custom_attributes_values.attributes_values = custom_attributes_values
obj.custom_attributes_values.save()
def _import_attachments(self, obj, issue, options):
users_bindings = options.get('users_bindings', {})
for attachment in issue['fields']['attachment']:
try:
data = self._client.raw_get(attachment['content'])
att = Attachment(
owner=users_bindings.get(attachment['author']['name'], self._user),
project=obj.project,
content_type=ContentType.objects.get_for_model(obj),
object_id=obj.id,
name=attachment['filename'],
size=attachment['size'],
created_date=attachment['created'],
is_deprecated=False,
)
att.attached_file.save(attachment['filename'], ContentFile(data), save=True)
except Exception:
print("ERROR getting attachment url {}".format(attachment['content']))
def _import_changelog(self, project, obj, issue, options):
obj.cummulative_attachments = []
for history in sorted(issue['changelog']['histories'], key=lambda h: h['created']):
self._import_history(project, obj, history, options)
def _import_history(self, project, obj, history, options):
key = make_key_from_model_object(obj)
typename = get_typename_for_model_class(obj.__class__)
history_data = self._transform_history_data(project, obj, history, options)
if history_data is None:
return
change_old = history_data['change_old']
change_new = history_data['change_new']
hist_type = history_data['hist_type']
comment = history_data['comment']
user = history_data['user']
diff = make_diff_from_dicts(change_old, change_new)
fdiff = FrozenDiff(key, diff, {})
values = make_diff_values(typename, fdiff)
values.update(history_data['update_values'])
entry = HistoryEntry.objects.create(
user=user,
project_id=obj.project.id,
key=key,
type=hist_type,
snapshot=None,
diff=fdiff.diff,
values=values,
comment=comment,
comment_html=mdrender(obj.project, comment),
is_hidden=False,
is_snapshot=False,
)
HistoryEntry.objects.filter(id=entry.id).update(created_at=history['created'])
return HistoryEntry.objects.get(id=entry.id)
def _transform_history_data(self, project, obj, history, options):
users_bindings = options.get('users_bindings', {})
user = {"pk": None, "name": history.get('author', {}).get('displayName', None)}
taiga_user = users_bindings.get(history.get('author', {}).get('key', None), None)
if taiga_user:
user = {"pk": taiga_user.id, "name": taiga_user.get_full_name()}
result = {
"change_old": {},
"change_new": {},
"update_values": {},
"hist_type": HistoryType.change,
"comment": "",
"user": user
}
custom_fields_by_names = {f["history_name"]: f for f in self.custom_fields}
has_data = False
for history_item in history['items']:
if history_item['field'] == "Attachment":
result['change_old']["attachments"] = []
for att in obj.cummulative_attachments:
result['change_old']["attachments"].append({
"id": 0,
"filename": att
})
if history_item['from'] is not None:
try:
idx = obj.cummulative_attachments.index(history_item['fromString'])
obj.cummulative_attachments.pop(idx)
except ValueError:
print("ERROR: Removing attachment that doesn't exist in the history ({})".format(history_item['fromString']))
if history_item['to'] is not None:
obj.cummulative_attachments.append(history_item['toString'])
result['change_new']["attachments"] = []
for att in obj.cummulative_attachments:
result['change_new']["attachments"].append({
"id": 0,
"filename": att
})
has_data = True
elif history_item['field'] == "description":
result['change_old']["description"] = history_item['fromString']
result['change_new']["description"] = history_item['toString']
result['change_old']["description_html"] = mdrender(obj.project, history_item['fromString'] or "")
result['change_new']["description_html"] = mdrender(obj.project, history_item['toString'] or "")
has_data = True
elif history_item['field'] == "Epic Link":
pass
elif history_item['field'] == "Workflow":
pass
elif history_item['field'] == "Link":
pass
elif history_item['field'] == "labels":
result['change_old']["tags"] = history_item['fromString'].split()
result['change_new']["tags"] = history_item['toString'].split()
has_data = True
elif history_item['field'] == "Rank":
pass
elif history_item['field'] == "RemoteIssueLink":
pass
elif history_item['field'] == "Sprint":
old_milestone = None
if history_item['fromString']:
try:
old_milestone = obj.project.milestones.get(name=history_item['fromString']).id
except Milestone.DoesNotExist:
old_milestone = -1
new_milestone = None
if history_item['toString']:
try:
new_milestone = obj.project.milestones.get(name=history_item['toString']).id
except Milestone.DoesNotExist:
new_milestone = -2
result['change_old']["milestone"] = old_milestone
result['change_new']["milestone"] = new_milestone
if old_milestone == -1 or new_milestone == -2:
result['update_values']["milestone"] = {}
if old_milestone == -1:
result['update_values']["milestone"]["-1"] = history_item['fromString']
if new_milestone == -2:
result['update_values']["milestone"]["-2"] = history_item['toString']
has_data = True
elif history_item['field'] == "status":
if isinstance(obj, Task):
try:
old_status = obj.project.task_statuses.get(name=history_item['fromString']).id
except Exception:
old_status = -1
try:
new_status = obj.project.task_statuses.get(name=history_item['toString']).id
except Exception:
new_status = -2
elif isinstance(obj, UserStory):
try:
old_status = obj.project.us_statuses.get(name=history_item['fromString']).id
except Exception:
old_status = -1
try:
new_status = obj.project.us_statuses.get(name=history_item['toString']).id
except Exception:
new_status = -2
elif isinstance(obj, Issue):
try:
old_status = obj.project.issue_statuses.get(name=history_item['fromString']).id
except Exception:
old_status = -1
try:
new_status = obj.project.us_statuses.get(name=history_item['toString']).id
except Exception:
new_status = -2
elif isinstance(obj, Epic):
try:
old_status = obj.project.epic_statuses.get(name=history_item['fromString']).id
except Exception:
old_status = -1
try:
new_status = obj.project.epic_statuses.get(name=history_item['toString']).id
except Exception:
new_status = -2
if old_status == -1 or new_status == -2:
result['update_values']["status"] = {}
if old_status == -1:
result['update_values']["status"]["-1"] = history_item['fromString']
if new_status == -2:
result['update_values']["status"]["-2"] = history_item['toString']
result['change_old']["status"] = old_status
result['change_new']["status"] = new_status
has_data = True
elif history_item['field'] == "Story Points":
old_points = None
if history_item['fromString']:
estimation = float(history_item['fromString'])
(old_points, _) = Points.objects.get_or_create(
project=project,
value=estimation,
defaults={
"name": str(estimation),
"order": estimation,
}
)
old_points = old_points.id
new_points = None
if history_item['toString']:
estimation = float(history_item['toString'])
(new_points, _) = Points.objects.get_or_create(
project=project,
value=estimation,
defaults={
"name": str(estimation),
"order": estimation,
}
)
new_points = new_points.id
result['change_old']["points"] = {project.roles.get(slug="main").id: old_points}
result['change_new']["points"] = {project.roles.get(slug="main").id: new_points}
has_data = True
elif history_item['field'] == "summary":
result['change_old']["subject"] = history_item['fromString']
result['change_new']["subject"] = history_item['toString']
has_data = True
elif history_item['field'] == "Epic Color":
if isinstance(obj, Epic):
result['change_old']["color"] = EPIC_COLORS.get(history_item['fromString'], None)
result['change_new']["color"] = EPIC_COLORS.get(history_item['toString'], None)
Epic.objects.filter(id=obj.id).update(
color=EPIC_COLORS.get(history_item['toString'], "#999999")
)
has_data = True
elif history_item['field'] == "assignee":
old_assigned_to = None
if history_item['from'] is not None:
old_assigned_to = users_bindings.get(history_item['from'], -1)
if old_assigned_to != -1:
old_assigned_to = old_assigned_to.id
new_assigned_to = None
if history_item['to'] is not None:
new_assigned_to = users_bindings.get(history_item['to'], -2)
if new_assigned_to != -2:
new_assigned_to = new_assigned_to.id
result['change_old']["assigned_to"] = old_assigned_to
result['change_new']["assigned_to"] = new_assigned_to
if old_assigned_to == -1 or new_assigned_to == -2:
result['update_values']["users"] = {}
if old_assigned_to == -1:
result['update_values']["users"]["-1"] = history_item['fromString']
if new_assigned_to == -2:
result['update_values']["users"]["-2"] = history_item['toString']
has_data = True
elif history_item['field'] in custom_fields_by_names:
custom_field = custom_fields_by_names[history_item['field']]
if isinstance(obj, Task):
field_obj = obj.project.taskcustomattributes.get(name=custom_field['taiga_field_name'])
elif isinstance(obj, UserStory):
field_obj = obj.project.userstorycustomattributes.get(name=custom_field['taiga_field_name'])
elif isinstance(obj, Issue):
field_obj = obj.project.issuecustomattributes.get(name=custom_field['taiga_field_name'])
elif isinstance(obj, Epic):
field_obj = obj.project.epiccustomattributes.get(name=custom_field['taiga_field_name'])
result['change_old']["custom_attributes"] = [{
"name": custom_field['taiga_field_name'],
"value": history_item['fromString'],
"id": field_obj.id
}]
result['change_new']["custom_attributes"] = [{
"name": custom_field['taiga_field_name'],
"value": history_item['toString'],
"id": field_obj.id
}]
has_data = True
else:
import pprint; pprint.pprint(history_item)
if not has_data:
return None
return result
def _cleanup(self, project, options):
for epic_custom_field in project.epiccustomattributes.all():
if project.epics.filter(custom_attributes_values__attributes_values__has_key=str(epic_custom_field.id)).count() == 0:
epic_custom_field.delete()
for us_custom_field in project.userstorycustomattributes.all():
if project.user_stories.filter(custom_attributes_values__attributes_values__has_key=str(us_custom_field.id)).count() == 0:
us_custom_field.delete()
for task_custom_field in project.taskcustomattributes.all():
if project.tasks.filter(custom_attributes_values__attributes_values__has_key=str(task_custom_field.id)).count() == 0:
task_custom_field.delete()
for issue_custom_field in project.issuecustomattributes.all():
if project.issues.filter(custom_attributes_values__attributes_values__has_key=str(issue_custom_field.id)).count() == 0:
issue_custom_field.delete()
@classmethod
def get_auth_url(cls, server, consumer_key, key_cert_data, verify=None):
if verify is None:
verify = server.startswith('https')
oauth = OAuth1(consumer_key, signature_method=SIGNATURE_RSA, rsa_key=key_cert_data)
r = requests.post(
server + '/plugins/servlet/oauth/request-token', verify=verify, auth=oauth)
request = dict(parse_qsl(r.text))
request_token = request['oauth_token']
request_token_secret = request['oauth_token_secret']
return (
request_token,
request_token_secret,
'{}/plugins/servlet/oauth/authorize?oauth_token={}'.format(server, request_token)
)
@classmethod
def get_access_token(cls, server, consumer_key, key_cert_data, request_token, request_token_secret, verify=False):
oauth = OAuth1(
consumer_key,
signature_method=SIGNATURE_RSA,
rsa_key=key_cert_data,
resource_owner_key=request_token,
resource_owner_secret=request_token_secret
)
r = requests.post(server + '/plugins/servlet/oauth/access-token', verify=verify, auth=oauth)
access = dict(parse_qsl(r.text))
return {
'access_token': access['oauth_token'],
'access_token_secret': access['oauth_token_secret'],
'consumer_key': consumer_key,
'key_cert': key_cert_data
}

View File

@ -0,0 +1,439 @@
from collections import OrderedDict
from django.template.defaultfilters import slugify
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.issues.models import Issue
from taiga.projects.epics.models import Epic, RelatedUserStory
from taiga.projects.history.services import take_snapshot
from taiga.timeline.rebuilder import rebuild_timeline
from taiga.timeline.models import Timeline
from .common import JiraImporterCommon
class JiraNormalImporter(JiraImporterCommon):
def list_projects(self):
return [{"id": project['id'],
"name": project['name'],
"description": project['description'],
"is_private": True,
"importer_type": "normal"} for project in self._client.get('/project', {"expand": "description"})]
def list_issue_types(self, project_id):
statuses = self._client.get("/project/{}/statuses".format(project_id))
return statuses
def import_project(self, project_id, options):
self.resolve_user_bindings(options)
project = self._import_project_data(project_id, options)
self._import_user_stories_data(project_id, project, options)
self._import_epics_data(project_id, project, options)
self._link_epics_with_user_stories(project_id, project, options)
self._import_issues_data(project_id, project, options)
self._cleanup(project, options)
Timeline.objects.filter(project=project).delete()
rebuild_timeline(None, None, project.id)
recalc_reference_counter(project)
return project
def _import_project_data(self, project_id, options):
project = self._client.get("/project/{}".format(project_id))
project_template = ProjectTemplate.objects.get(slug=options['template'])
epic_statuses = OrderedDict()
for issue_type in options.get('types_bindings', {}).get("epic", []):
for status in issue_type['statuses']:
epic_statuses[status['name']] = status
us_statuses = OrderedDict()
for issue_type in options.get('types_bindings', {}).get("us", []):
for status in issue_type['statuses']:
us_statuses[status['name']] = status
task_statuses = OrderedDict()
for issue_type in options.get('types_bindings', {}).get("task", []):
for status in issue_type['statuses']:
task_statuses[status['name']] = status
issue_statuses = OrderedDict()
for issue_type in options.get('types_bindings', {}).get("issue", []):
for status in issue_type['statuses']:
issue_statuses[status['name']] = status
counter = 0
if epic_statuses:
project_template.epic_statuses = []
project_template.is_epics_activated = True
for epic_status in epic_statuses.values():
project_template.epic_statuses.append({
"name": epic_status['name'],
"slug": slugify(epic_status['name']),
"is_closed": False,
"color": "#999999",
"order": counter,
})
counter += 1
if epic_statuses:
project_template.default_options["epic_status"] = list(epic_statuses.values())[0]['name']
project_template.points = [{
"value": None,
"name": "?",
"order": 0,
}]
main_permissions = project_template.roles[0]['permissions']
project_template.roles = [{
"name": "Main",
"slug": "main",
"computable": True,
"permissions": main_permissions,
"order": 70,
}]
counter = 0
if us_statuses:
project_template.us_statuses = []
for us_status in us_statuses.values():
project_template.us_statuses.append({
"name": us_status['name'],
"slug": slugify(us_status['name']),
"is_closed": False,
"is_archived": False,
"color": "#999999",
"wip_limit": None,
"order": counter,
})
counter += 1
if us_statuses:
project_template.default_options["us_status"] = list(us_statuses.values())[0]['name']
counter = 0
if task_statuses:
project_template.task_statuses = []
for task_status in task_statuses.values():
project_template.task_statuses.append({
"name": task_status['name'],
"slug": slugify(task_status['name']),
"is_closed": False,
"color": "#999999",
"order": counter,
})
counter += 1
if task_statuses:
project_template.default_options["task_status"] = list(task_statuses.values())[0]['name']
counter = 0
if issue_statuses:
project_template.issue_statuses = []
for issue_status in issue_statuses.values():
project_template.issue_statuses.append({
"name": issue_status['name'],
"slug": slugify(issue_status['name']),
"is_closed": False,
"color": "#999999",
"order": counter,
})
counter += 1
if issue_statuses:
project_template.default_options["issue_status"] = list(issue_statuses.values())[0]['name']
main_permissions = project_template.roles[0]['permissions']
project_template.roles = [{
"name": "Main",
"slug": "main",
"computable": True,
"permissions": main_permissions,
"order": 70,
}]
project = Project.objects.create(
name=options.get('name', None) or project['name'],
description=options.get('description', None) or project.get('description', ''),
owner=self._user,
creation_template=project_template,
is_private=options.get('is_private', False),
)
self._create_custom_fields(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,
)
return project
def _import_user_stories_data(self, project_id, project, options):
users_bindings = options.get('users_bindings', {})
types = options.get('types_bindings', {}).get("us", [])
for issue_type in types:
counter = 0
offset = 0
while True:
issues = self._client.get("/search", {
"jql": "project={} AND issuetype={}".format(project_id, issue_type['id']),
"startAt": offset,
"fields": "*all",
"expand": "changelog,attachment",
})
offset += issues['maxResults']
for issue in issues['issues']:
issue['fields']['issuelinks'] += self._client.get("/issue/{}/remotelink".format(issue['key']))
assigned_to = users_bindings.get(issue['fields']['assignee']['key'] if issue['fields']['assignee'] else None, None)
owner = users_bindings.get(issue['fields']['creator']['key'] if issue['fields']['creator'] else None, self._user)
external_reference = None
if options.get('keep_external_reference', False):
external_reference = ["jira", issue['fields']['url']]
us = UserStory.objects.create(
project=project,
owner=owner,
assigned_to=assigned_to,
status=project.us_statuses.get(name=issue['fields']['status']['name']),
kanban_order=counter,
sprint_order=counter,
backlog_order=counter,
subject=issue['fields']['summary'],
description=issue['fields']['description'] or '',
tags=issue['fields']['labels'],
external_reference=external_reference,
)
points_value = issue['fields'].get(self.greenhopper_fields.get('points', None), None)
if points_value:
(points, _) = Points.objects.get_or_create(
project=project,
value=points_value,
defaults={
"name": str(points_value),
"order": points_value,
}
)
RolePoints.objects.filter(user_story=us, role__slug="main").update(points_id=points.id)
else:
points = Points.objects.get(project=project, value__isnull=True)
RolePoints.objects.filter(user_story=us, role__slug="main").update(points_id=points.id)
self._import_to_custom_fields(us, issue, options)
us.ref = issue['key'].split("-")[1]
UserStory.objects.filter(id=us.id).update(
ref=us.ref,
modified_date=issue['fields']['updated'],
created_date=issue['fields']['created']
)
take_snapshot(us, comment="", user=None, delete=False)
self._import_subtasks(project_id, project, us, issue, options)
self._import_comments(us, issue, options)
self._import_attachments(us, issue, options)
self._import_changelog(project, us, issue, options)
counter += 1
if len(issues['issues']) < issues['maxResults']:
break
def _import_subtasks(self, project_id, project, us, issue, options):
users_bindings = options.get('users_bindings', {})
if len(issue['fields']['subtasks']) == 0:
return
counter = 0
offset = 0
while True:
issues = self._client.get("/search", {
"jql": "parent={}".format(issue['key']),
"startAt": offset,
"fields": "*all",
"expand": "changelog,attachment",
})
offset += issues['maxResults']
for issue in issues['issues']:
issue['fields']['issuelinks'] += self._client.get("/issue/{}/remotelink".format(issue['key']))
assigned_to = users_bindings.get(issue['fields']['assignee']['key'] if issue['fields']['assignee'] else None, None)
owner = users_bindings.get(issue['fields']['creator']['key'] if issue['fields']['creator'] else None, self._user)
external_reference = None
if options.get('keep_external_reference', False):
external_reference = ["jira", issue['fields']['url']]
task = Task.objects.create(
user_story=us,
project=project,
owner=owner,
assigned_to=assigned_to,
status=project.task_statuses.get(name=issue['fields']['status']['name']),
subject=issue['fields']['summary'],
description=issue['fields']['description'] or '',
tags=issue['fields']['labels'],
external_reference=external_reference,
)
self._import_to_custom_fields(task, issue, options)
task.ref = issue['key'].split("-")[1]
Task.objects.filter(id=task.id).update(
ref=task.ref,
modified_date=issue['fields']['updated'],
created_date=issue['fields']['created']
)
take_snapshot(task, comment="", user=None, delete=False)
for subtask in issue['fields']['subtasks']:
print("WARNING: Ignoring subtask {} because parent isn't a User Story".format(subtask['key']))
self._import_comments(task, issue, options)
self._import_attachments(task, issue, options)
self._import_changelog(project, task, issue, options)
counter += 1
if len(issues['issues']) < issues['maxResults']:
break
def _import_issues_data(self, project_id, project, options):
users_bindings = options.get('users_bindings', {})
types = options.get('types_bindings', {}).get("issue", [])
for issue_type in types:
counter = 0
offset = 0
while True:
issues = self._client.get("/search", {
"jql": "project={} AND issuetype={}".format(project_id, issue_type['id']),
"startAt": offset,
"fields": "*all",
"expand": "changelog,attachment",
})
offset += issues['maxResults']
for issue in issues['issues']:
issue['fields']['issuelinks'] += self._client.get("/issue/{}/remotelink".format(issue['key']))
assigned_to = users_bindings.get(issue['fields']['assignee']['key'] if issue['fields']['assignee'] else None, None)
owner = users_bindings.get(issue['fields']['creator']['key'] if issue['fields']['creator'] else None, self._user)
external_reference = None
if options.get('keep_external_reference', False):
external_reference = ["jira", issue['fields']['url']]
taiga_issue = Issue.objects.create(
project=project,
owner=owner,
assigned_to=assigned_to,
status=project.issue_statuses.get(name=issue['fields']['status']['name']),
subject=issue['fields']['summary'],
description=issue['fields']['description'] or '',
tags=issue['fields']['labels'],
external_reference=external_reference,
)
self._import_to_custom_fields(taiga_issue, issue, options)
taiga_issue.ref = issue['key'].split("-")[1]
Issue.objects.filter(id=taiga_issue.id).update(
ref=taiga_issue.ref,
modified_date=issue['fields']['updated'],
created_date=issue['fields']['created']
)
take_snapshot(taiga_issue, comment="", user=None, delete=False)
for subtask in issue['fields']['subtasks']:
print("WARNING: Ignoring subtask {} because parent isn't a User Story".format(subtask['key']))
self._import_comments(taiga_issue, issue, options)
self._import_attachments(taiga_issue, issue, options)
self._import_changelog(project, taiga_issue, issue, options)
counter += 1
if len(issues['issues']) < issues['maxResults']:
break
def _import_epics_data(self, project_id, project, options):
users_bindings = options.get('users_bindings', {})
types = options.get('types_bindings', {}).get("epic", [])
for issue_type in types:
counter = 0
offset = 0
while True:
issues = self._client.get("/search", {
"jql": "project={} AND issuetype={}".format(project_id, issue_type['id']),
"startAt": offset,
"fields": "*all",
"expand": "changelog,attachment",
})
offset += issues['maxResults']
for issue in issues['issues']:
issue['fields']['issuelinks'] += self._client.get("/issue/{}/remotelink".format(issue['key']))
assigned_to = users_bindings.get(issue['fields']['assignee']['key'] if issue['fields']['assignee'] else None, None)
owner = users_bindings.get(issue['fields']['creator']['key'] if issue['fields']['creator'] else None, self._user)
external_reference = None
if options.get('keep_external_reference', False):
external_reference = ["jira", issue['fields']['url']]
epic = Epic.objects.create(
project=project,
owner=owner,
assigned_to=assigned_to,
status=project.epic_statuses.get(name=issue['fields']['status']['name']),
subject=issue['fields']['summary'],
description=issue['fields']['description'] or '',
epics_order=counter,
tags=issue['fields']['labels'],
external_reference=external_reference,
)
self._import_to_custom_fields(epic, issue, options)
epic.ref = issue['key'].split("-")[1]
Epic.objects.filter(id=epic.id).update(
ref=epic.ref,
modified_date=issue['fields']['updated'],
created_date=issue['fields']['created']
)
take_snapshot(epic, comment="", user=None, delete=False)
for subtask in issue['fields']['subtasks']:
print("WARNING: Ignoring subtask {} because parent isn't a User Story".format(subtask['key']))
self._import_comments(epic, issue, options)
self._import_attachments(epic, issue, options)
issue_with_changelog = self._client.get("/issue/{}".format(issue['key']), {
"expand": "changelog"
})
self._import_changelog(project, epic, issue_with_changelog, options)
counter += 1
if len(issues['issues']) < issues['maxResults']:
break
def _link_epics_with_user_stories(self, project_id, project, options):
types = options.get('types_bindings', {}).get("us", [])
for issue_type in types:
offset = 0
while True:
issues = self._client.get("/search", {
"jql": "project={} AND issuetype={}".format(project_id, issue_type['id']),
"startAt": offset
})
offset += issues['maxResults']
for issue in issues['issues']:
epic_key = issue['fields'][self.greenhopper_fields['link']]
if epic_key:
epic = project.epics.get(ref=int(epic_key.split("-")[1]))
us = project.user_stories.get(ref=int(issue['key'].split("-")[1]))
RelatedUserStory.objects.create(
user_story=us,
epic=epic,
order=1
)
if len(issues['issues']) < issues['maxResults']:
break

View File

@ -0,0 +1,61 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2014-2016 Andrey Antukh <niwi@niwi.nz>
# Copyright (C) 2014-2016 Jesús Espino <jespinog@gmail.com>
# Copyright (C) 2014-2016 David Barragán <bameda@dbarragan.com>
# Copyright (C) 2014-2016 Alejandro Alonso <alejandro.alonso@kaleidos.net>
# 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 <http://www.gnu.org/licenses/>.
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 .normal import JiraNormalImporter
logger = logging.getLogger('taiga.importers.jira')
@app.task(bind=True)
def import_project(self, user_id, url, token, project_id, options, importer_type):
user = User.object.get(id=user_id)
if importer_type == "agile":
importer = JiraAgileImporter(user, url, token)
else:
importer = JiraNormalImporter(user, url, token)
try:
project = importer.import_project(project_id, options)
except Exception as e:
# Error
ctx = {
"user": user,
"error_subject": _("Error importing jira project"),
"error_message": _("Error importing jira project"),
"project": project_id,
"exception": e
}
email = mail_builder.jira_import_error(admin, ctx)
email.send()
logger.error('Error importing jira project %s (by %s)', project_id, user, exc_info=sys.exc_info())
else:
ctx = {
"project": project,
"user": user,
}
email = mail_builder.jira_import_success(user, ctx)
email.send()

View File

@ -0,0 +1,146 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2014-2016 Andrey Antukh <niwi@niwi.nz>
# Copyright (C) 2014-2016 Jesús Espino <jespinog@gmail.com>
# Copyright (C) 2014-2016 David Barragán <bameda@dbarragan.com>
# Copyright (C) 2014-2016 Alejandro Alonso <alejandro.alonso@kaleidos.net>
# 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 <http://www.gnu.org/licenses/>.
from django.core.management.base import BaseCommand
from django.db.models import Q
from django.conf import settings
from taiga.importers.jira.agile import JiraAgileImporter
from taiga.importers.jira.normal import JiraNormalImporter
from taiga.users.models import User
from taiga.projects.services import projects as service
import unittest.mock
import timeit
import json
class Command(BaseCommand):
def add_arguments(self, parser):
parser.add_argument('--token', dest="token", type=str,
help='Auth token')
parser.add_argument('--server', dest="server", type=str,
help='Server address (default: https://jira.atlassian.com)',
default="https://jira.atlassian.com")
parser.add_argument('--project-id', dest="project_id", type=str,
help='Project ID or full name (ex: taigaio/taiga-back)')
parser.add_argument('--project-type', dest="project_type", type=str,
help='Project type in jira: project or board')
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")
server = options.get("server")
if options.get('token', None) == "anon":
token = None
elif options.get('token', None):
token = json.loads(options.get('token'))
else:
(rtoken, rtoken_secret, url) = JiraNormalImporter.get_auth_url(server, settings.JIRA_CONSUMER_KEY, settings.JIRA_CERT, True)
print(url)
code = input("Go to the url and get back the code")
token = JiraNormalImporter.get_access_token(server, settings.JIRA_CONSUMER_KEY, settings.JIRA_CERT, rtoken, rtoken_secret, True)
print("Auth token: {}".format(json.dumps(token)))
if options.get('project_type', None) is None:
print("Select the type of project to import (project or board): ")
project_type = input("Project type: ")
else:
project_type = options.get('project_type')
if project_type not in ["project", "board"]:
print("ERROR: Bad project type.")
return
if project_type == "project":
importer = JiraNormalImporter(admin, server, token)
else:
importer = JiraAgileImporter(admin, server, 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['id'], project['name']))
project_id = input("Project id or key: ")
users_bindings = {}
if options.get('ask_for_users', None):
print("Add the username or email for next jira users:")
for user in importer.list_users():
try:
users_bindings[user['key']] = User.objects.get(Q(email=user['email']))
break
except User.DoesNotExist:
pass
while True:
username_or_email = input("{}: ".format(user['full_name']))
if username_or_email == "":
break
try:
users_bindings[user['key']] = 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'),
}
if project_type == "project":
print("Bind jira issue types to (epic, us, issue)")
types_bindings = {
"epic": [],
"us": [],
"task": [],
"issue": [],
}
for issue_type in importer.list_issue_types(project_id):
while True:
if issue_type['subtask']:
types_bindings['task'].append(issue_type)
break
taiga_type = input("{}: ".format(issue_type['name']))
if taiga_type not in ['epic', 'us', 'issue']:
print("use a valid taiga type (epic, us, issue)")
continue
types_bindings[taiga_type].append(issue_type)
break
options["types_bindings"] = types_bindings
importer.import_project(project_id, options)

View File

@ -285,8 +285,10 @@ router.register(r"application-tokens", ApplicationToken, base_name="application-
# Third party importers
from taiga.importers.trello.api import TrelloImporterViewSet
from taiga.importers.jira.api import JiraImporterViewSet
router.register(r"importers/trello", TrelloImporterViewSet, base_name="importers-trello")
router.register(r"importers/jira", JiraImporterViewSet, base_name="importers-jira")
# Stats

View File

@ -0,0 +1,259 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2014-2016 Andrey Antukh <niwi@niwi.nz>
# Copyright (C) 2014-2016 Jesús Espino <jespinog@gmail.com>
# Copyright (C) 2014-2016 David Barragán <bameda@dbarragan.com>
# Copyright (C) 2014-2016 Alejandro Alonso <alejandro.alonso@kaleidos.net>
# 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 <http://www.gnu.org/licenses/>.
import pytest
import json
from unittest import mock
from django.core.urlresolvers import reverse
from .. import factories as f
from taiga.base.utils import json
from taiga.base import exceptions as exc
from taiga.users.models import AuthData
pytestmark = pytest.mark.django_db
fake_token = "access.secret"
def test_auth_url(client):
user = f.UserFactory.create()
client.login(user)
url = reverse("importers-jira-auth-url")+"?url=http://jiraserver"
with mock.patch('taiga.importers.jira.api.JiraNormalImporter') as JiraNormalImporter:
JiraNormalImporter.get_auth_url.return_value = ("test_oauth_token", "test_oauth_secret", "http://jira-server-url")
response = client.get(url, content_type="application/json")
auth_data = user.auth_data.get(key="jira-oauth")
assert auth_data.extra['oauth_token'] == "test_oauth_token"
assert auth_data.extra['oauth_secret'] == "test_oauth_secret"
assert auth_data.extra['url'] == "http://jiraserver"
assert response.status_code == 200
assert 'url' in response.data
assert response.data['url'] == "http://jira-server-url"
def test_authorize(client):
user = f.UserFactory.create()
client.login(user)
url = reverse("importers-jira-auth-url")
authorize_url = reverse("importers-jira-authorize")
AuthData.objects.get_or_create(
user=user,
key="jira-oauth",
value="",
extra={
"oauth_token": "test-oauth-token",
"oauth_secret": "test-oauth-secret",
"url": "http://jiraserver",
}
)
with mock.patch('taiga.importers.jira.api.JiraNormalImporter') as JiraNormalImporter:
JiraNormalImporter.get_access_token.return_value = {
"access_token": "test-access-token",
"access_token_secret": "test-access-token-secret"
}
response = client.post(authorize_url, content_type="application/json", data={})
assert response.status_code == 200
assert 'token' in response.data
assert response.data['token'] == "test-access-token.test-access-token-secret"
assert 'url' in response.data
assert response.data['url'] == "http://jiraserver"
def test_authorize_without_token_and_secret(client):
user = f.UserFactory.create()
client.login(user)
authorize_url = reverse("importers-jira-authorize")
AuthData.objects.filter(user=user, key="jira-oauth").delete()
response = client.post(authorize_url, content_type="application/json", data={})
assert response.status_code == 400
assert 'token' not in response.data
def test_import_jira_list_users(client, settings):
user = f.UserFactory.create()
client.login(user)
url = reverse("importers-jira-list-users")
with mock.patch('taiga.importers.jira.api.JiraNormalImporter') as JiraNormalImporterMock:
instance = mock.Mock()
instance.list_users.return_value = [
{"id": 1, "fullName": "user1", "email": None},
{"id": 2, "fullName": "user2", "email": None}
]
JiraNormalImporterMock.return_value = instance
response = client.post(url, content_type="application/json", data=json.dumps({"token": "access.secret", "url": "http://jiraserver", "project": 1}))
assert response.status_code == 200
assert response.data[0]["id"] == 1
assert response.data[1]["id"] == 2
def test_import_jira_list_users_without_project(client, settings):
user = f.UserFactory.create()
client.login(user)
url = reverse("importers-jira-list-users")
with mock.patch('taiga.importers.jira.api.JiraNormalImporter') as JiraNormalImporterMock:
instance = mock.Mock()
instance.list_users.return_value = [
{"id": 1, "fullName": "user1", "email": None},
{"id": 2, "fullName": "user2", "email": None}
]
JiraNormalImporterMock.return_value = instance
response = client.post(url, content_type="application/json", data=json.dumps({"token": "access.secret", "url": "http://jiraserver"}))
assert response.status_code == 400
def test_import_jira_list_users_with_problem_on_request(client, settings):
user = f.UserFactory.create()
client.login(user)
url = reverse("importers-jira-list-users")
with mock.patch('taiga.importers.jira.common.JiraClient') as JiraClientMock:
instance = mock.Mock()
instance.get.side_effect = exc.WrongArguments("Invalid Request")
JiraClientMock.return_value = instance
response = client.post(url, content_type="application/json", data=json.dumps({"token": "access.secret", "url": "http://jiraserver", "project": 1}))
assert response.status_code == 400
def test_import_jira_list_projects(client, settings):
user = f.UserFactory.create()
client.login(user)
url = reverse("importers-jira-list-projects")
with mock.patch('taiga.importers.jira.api.JiraNormalImporter') as JiraNormalImporterMock:
with mock.patch('taiga.importers.jira.api.JiraAgileImporter') as JiraAgileImporterMock:
instance = mock.Mock()
instance.list_projects.return_value = [{"name": "project1"}, {"name": "project2"}]
JiraNormalImporterMock.return_value = instance
instance_agile = mock.Mock()
instance_agile.list_projects.return_value = [{"name": "agile1"}, {"name": "agile2"}]
JiraAgileImporterMock.return_value = instance_agile
response = client.post(url, content_type="application/json", data=json.dumps({"token": "access.secret", "url": "http://jiraserver"}))
assert response.status_code == 200
assert response.data[0] == {"name": "agile1"}
assert response.data[1] == {"name": "agile2"}
assert response.data[2] == {"name": "project1"}
assert response.data[3] == {"name": "project2"}
def test_import_jira_list_projects_with_problem_on_request(client, settings):
user = f.UserFactory.create()
client.login(user)
url = reverse("importers-jira-list-projects")
with mock.patch('taiga.importers.jira.common.JiraClient') as JiraClientMock:
instance = mock.Mock()
instance.get.side_effect = exc.WrongArguments("Invalid Request")
JiraClientMock.return_value = instance
response = client.post(url, content_type="application/json", data=json.dumps({"token": "access.secret", "url": "http://jiraserver"}))
assert response.status_code == 400
def test_import_jira_project_without_project_id(client, settings):
settings.CELERY_ENABLED = True
user = f.UserFactory.create()
client.login(user)
url = reverse("importers-jira-import-project")
with mock.patch('taiga.importers.jira.tasks.JiraNormalImporter') as JiraNormalImporterMock:
response = client.post(url, content_type="application/json", data=json.dumps({"token": "access.secret", "url": "http://jiraserver"}))
assert response.status_code == 400
def test_import_jira_project_without_url(client, settings):
settings.CELERY_ENABLED = True
user = f.UserFactory.create()
client.login(user)
url = reverse("importers-jira-import-project")
with mock.patch('taiga.importers.jira.tasks.JiraNormalImporter') as JiraNormalImporterMock:
response = client.post(url, content_type="application/json", data=json.dumps({"token": "access.secret", "project_id": 1}))
assert response.status_code == 400
def test_import_jira_project_with_celery_enabled(client, settings):
settings.CELERY_ENABLED = True
user = f.UserFactory.create()
project = f.ProjectFactory.create(slug="async-imported-project")
client.login(user)
url = reverse("importers-jira-import-project")
with mock.patch('taiga.importers.jira.api.JiraNormalImporter') as ApiJiraNormalImporterMock:
with mock.patch('taiga.importers.jira.tasks.JiraNormalImporter') as TasksJiraNormalImporterMock:
TasksJiraNormalImporterMock.return_value.import_project.return_value = project
ApiJiraNormalImporterMock.return_value.list_issue_types.return_value = []
response = client.post(url, content_type="application/json", data=json.dumps({"token": "access.secret", "url": "http://jiraserver", "project": 1}))
assert response.status_code == 202
assert "task_id" in response.data
def test_import_jira_project_with_celery_disabled(client, settings):
settings.CELERY_ENABLED = False
user = f.UserFactory.create()
project = f.ProjectFactory.create(slug="imported-project")
client.login(user)
url = reverse("importers-jira-import-project")
with mock.patch('taiga.importers.jira.api.JiraNormalImporter') as JiraNormalImporterMock:
instance = mock.Mock()
instance.import_project.return_value = project
instance.list_issue_types.return_value = []
JiraNormalImporterMock.return_value = instance
response = client.post(url, content_type="application/json", data=json.dumps({"token": "access.secret", "url": "http://jiraserver", "project": 1}))
assert response.status_code == 200
assert "slug" in response.data
assert response.data['slug'] == "imported-project"