Add jira importer
parent
f7595b65cc
commit
723034d373
|
@ -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"
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
})
|
|
@ -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
|
||||
}
|
|
@ -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
|
|
@ -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()
|
|
@ -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)
|
|
@ -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
|
||||
|
|
|
@ -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"
|
Loading…
Reference in New Issue