Merge pull request #910 from taigaio/github-import

Adding Jira, Trello, Asana, PivotalTracker and Github importers to back
remotes/origin/issue/4217/improving-mail-design
David Barragán Merino 2017-02-20 12:29:22 +01:00 committed by GitHub
commit f9adc543d4
44 changed files with 6729 additions and 29 deletions

View File

@ -14,6 +14,12 @@
- Add japanese (ja) translation. - Add japanese (ja) translation.
- Add korean (ko) translation. - Add korean (ko) translation.
- Add chinese simplified (zh-Hans) translation. - Add chinese simplified (zh-Hans) translation.
- Third party services project importers:
- Trello
- Jira 7
- Github
- Asana
- Pivotal Tracker
### Misc ### Misc
- API: - API:

View File

@ -17,6 +17,8 @@ Markdown==2.6.7
fn==0.4.3 fn==0.4.3
diff-match-patch==20121119 diff-match-patch==20121119
requests==2.12.4 requests==2.12.4
requests-oauthlib==0.6.2
webcolors==1.5
django-sr==0.0.4 django-sr==0.0.4
easy-thumbnails==2.3 easy-thumbnails==2.3
celery==3.1.24 celery==3.1.24
@ -35,3 +37,6 @@ netaddr==0.7.18
serpy==0.1.1 serpy==0.1.1
psd-tools==1.4 psd-tools==1.4
CairoSVG==2.0.1 CairoSVG==2.0.1
cryptography==1.7.1
PyJWT==1.4.2
asana==0.6.2

View File

@ -318,6 +318,7 @@ INSTALLED_APPS = [
"taiga.hooks.bitbucket", "taiga.hooks.bitbucket",
"taiga.hooks.gogs", "taiga.hooks.gogs",
"taiga.webhooks", "taiga.webhooks",
"taiga.importers",
"djmail", "djmail",
"django_jinja", "django_jinja",
@ -560,6 +561,30 @@ MAX_PENDING_MEMBERSHIPS = 30 # Max number of unconfirmed memberships in a projec
from .sr import * from .sr import *
IMPORTERS = {
"github": {
"active": False,
"client_id": "",
"client_secret": "",
},
"trello": {
"active": False,
"api_key": "",
"secret_key": "",
},
"jira": {
"active": False,
"consumer_key": "",
"cert": "",
"pub_cert": "",
},
"asana": {
"active": False,
"callback_url": "",
"app_id": "",
"app_secret": "",
}
}
# NOTE: DON'T INSERT MORE SETTINGS AFTER THIS LINE # NOTE: DON'T INSERT MORE SETTINGS AFTER THIS LINE
TEST_RUNNER="django.test.runner.DiscoverRunner" TEST_RUNNER="django.test.runner.DiscoverRunner"

View File

@ -153,3 +153,43 @@ DATABASES = {
# To use celery in memory # To use celery in memory
#CELERY_ENABLED = True #CELERY_ENABLED = True
#CELERY_ALWAYS_EAGER = True #CELERY_ALWAYS_EAGER = True
#########################################
## IMPORTERS
#########################################
# Configuration for the GitHub importer
# Remember to enable it in the front client too.
#IMPORTERS["github"] = {
# "active": True, # Enable or disable the importer
# "client_id": "XXXXXX_get_a_valid_client_id_from_github_XXXXXX",
# "client_secret": "XXXXXX_get_a_valid_client_secret_from_github_XXXXXX"
#}
# Configuration for the Trello importer
# Remember to enable it in the front client too.
#IMPORTERS["trello"] = {
# "active": True, # Enable or disable the importer
# "api_key": "XXXXXX_get_a_valid_api_key_from_trello_XXXXXX",
# "secret_key": "XXXXXX_get_a_valid_secret_key_from_trello_XXXXXX"
#}
# Configuration for the Jira importer
# Remember to enable it in the front client too.
#IMPORTERS["jira"] = {
# "active": True, # Enable or disable the importer
# "consumer_key": "XXXXXX_get_a_valid_consumer_key_from_jira_XXXXXX",
# "cert": "XXXXXX_get_a_valid_cert_from_jira_XXXXXX",
# "pub_cert": "XXXXXX_get_a_valid_pub_cert_from_jira_XXXXXX"
#}
# Configuration for the Asane importer
# Remember to enable it in the front client too.
#IMPORTERS["asana"] = {
# "active": True, # Enable or disable the importer
# "callback_url": "{}://{}/project/new/import/asana".format(SITES["front"]["scheme"],
# SITES["front"]["domain"]),
# "app_id": "XXXXXX_get_a_valid_app_id_from_asana_XXXXXX",
# "app_secret": "XXXXXX_get_a_valid_app_secret_from_asana_XXXXXX"
#}

View File

@ -38,3 +38,9 @@ REST_FRAMEWORK["DEFAULT_THROTTLE_RATES"] = {
"register-success": None, "register-success": None,
"user-detail": None, "user-detail": None,
} }
IMPORTERS['github']['active'] = True
IMPORTERS['jira']['active'] = True
IMPORTERS['asana']['active'] = True
IMPORTERS['trello']['active'] = True

View File

@ -23,6 +23,8 @@ urls = {
"login": "/login", "login": "/login",
"register": "/register", "register": "/register",
"forgot-password": "/forgot-password", "forgot-password": "/forgot-password",
"new-project": "/project/new",
"new-project-import": "/project/new/import/{0}",
"change-password": "/change-password/{0}", # user.token "change-password": "/change-password/{0}", # user.token
"change-email": "/change-email/{0}", # user.email_token "change-email": "/change-email/{0}", # user.email_token

39
taiga/importers/api.py Normal file
View File

@ -0,0 +1,39 @@
# -*- 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 taiga.base.api import viewsets
from taiga.base.decorators import list_route
class BaseImporterViewSet(viewsets.ViewSet):
@list_route(methods=["GET"])
def list_users(self, request, *args, **kwargs):
raise NotImplementedError
@list_route(methods=["GET"])
def list_projects(self, request, *args, **kwargs):
raise NotImplementedError
@list_route(methods=["POST"])
def import_project(self, request, *args, **kwargs):
raise NotImplementedError
@list_route(methods=["GET"])
def auth_url(self, request, *args, **kwargs):
raise NotImplementedError
@list_route(methods=["POST"])
def authorize(self, request, *args, **kwargs):
raise NotImplementedError

View File

@ -0,0 +1,145 @@
# -*- 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.projects.serializers import ProjectSerializer
from taiga.importers import permissions, exceptions
from taiga.importers.services import resolve_users_bindings
from .importer import AsanaImporter
from . import tasks
class AsanaImporterViewSet(viewsets.ViewSet):
permission_classes = (permissions.ImporterPermission,)
@list_route(methods=["POST"])
def list_users(self, request, *args, **kwargs):
self.check_permissions(request, "list_users", None)
token = request.DATA.get('token', None)
project_id = request.DATA.get('project', None)
if not project_id:
raise exc.WrongArguments(_("The project param is needed"))
importer = AsanaImporter(request.user, token)
try:
users = importer.list_users(project_id)
except exceptions.InvalidRequest:
raise exc.BadRequest(_('Invalid Asana API request'))
except exceptions.FailedRequest:
raise exc.BadRequest(_('Failed to make the request to Asana API'))
for user in users:
if user['detected_user']:
user['user'] = {
'id': user['detected_user'].id,
'full_name': user['detected_user'].get_full_name(),
'gravatar_id': get_user_gravatar_id(user['detected_user']),
'photo': get_user_photo_url(user['detected_user']),
}
del(user['detected_user'])
return response.Ok(users)
@list_route(methods=["POST"])
def list_projects(self, request, *args, **kwargs):
self.check_permissions(request, "list_projects", None)
token = request.DATA.get('token', None)
importer = AsanaImporter(request.user, token)
try:
projects = importer.list_projects()
except exceptions.InvalidRequest:
raise exc.BadRequest(_('Invalid Asana API request'))
except exceptions.FailedRequest:
raise exc.BadRequest(_('Failed to make the request to Asana API'))
return response.Ok(projects)
@list_route(methods=["POST"])
def import_project(self, request, *args, **kwargs):
self.check_permissions(request, "import_project", None)
token = request.DATA.get('token', None)
project_id = request.DATA.get('project', None)
if not project_id:
raise exc.WrongArguments(_("The project param is needed"))
options = {
"name": request.DATA.get('name', None),
"description": request.DATA.get('description', None),
"template": request.DATA.get('template', "scrum"),
"users_bindings": resolve_users_bindings(request.DATA.get("users_bindings", {})),
"keep_external_reference": request.DATA.get("keep_external_reference", False),
"is_private": request.DATA.get("is_private", False),
}
if settings.CELERY_ENABLED:
task = tasks.import_project.delay(request.user.id, token, project_id, options)
return response.Accepted({"task_id": task.id})
importer = AsanaImporter(request.user, token)
project = importer.import_project(project_id, options)
project_data = {
"slug": project.slug,
"my_permissions": ["view_us"],
"is_backlog_activated": project.is_backlog_activated,
"is_kanban_activated": project.is_kanban_activated,
}
return response.Ok(project_data)
@list_route(methods=["GET"])
def auth_url(self, request, *args, **kwargs):
self.check_permissions(request, "auth_url", None)
url = AsanaImporter.get_auth_url(
settings.IMPORTERS.get('asana', {}).get('app_id', None),
settings.IMPORTERS.get('asana', {}).get('app_secret', None),
settings.IMPORTERS.get('asana', {}).get('callback_url', None)
)
return response.Ok({"url": url})
@list_route(methods=["POST"])
def authorize(self, request, *args, **kwargs):
self.check_permissions(request, "authorize", None)
code = request.DATA.get('code', None)
if code is None:
raise exc.BadRequest(_("Code param needed"))
try:
asana_token = AsanaImporter.get_access_token(
code,
settings.IMPORTERS.get('asana', {}).get('app_id', None),
settings.IMPORTERS.get('asana', {}).get('app_secret', None),
settings.IMPORTERS.get('asana', {}).get('callback_url', None)
)
except exceptions.InvalidRequest:
raise exc.BadRequest(_('Invalid Asana API request'))
except exceptions.FailedRequest:
raise exc.BadRequest(_('Failed to make the request to Asana API'))
return response.Ok({"token": asana_token})

View File

@ -0,0 +1,352 @@
import requests
import asana
import json
from django.core.files.base import ContentFile
from django.contrib.contenttypes.models import ContentType
from taiga.projects.models import Project, ProjectTemplate, Membership
from taiga.projects.userstories.models import UserStory
from taiga.projects.tasks.models import Task
from taiga.projects.attachments.models import Attachment
from taiga.projects.history.services import take_snapshot
from taiga.projects.history.models import HistoryEntry
from taiga.projects.custom_attributes.models import UserStoryCustomAttribute, TaskCustomAttribute
from taiga.users.models import User
from taiga.timeline.rebuilder import rebuild_timeline
from taiga.timeline.models import Timeline
from taiga.importers import exceptions
class AsanaClient(asana.Client):
def request(self, method, path, **options):
try:
return super().request(method, path, **options)
except asana.error.AsanaError:
raise exceptions.InvalidRequest()
except Exception as e:
raise exceptions.FailedRequest()
class AsanaImporter:
def __init__(self, user, token, import_closed_data=False):
self._import_closed_data = import_closed_data
self._user = user
self._client = AsanaClient.oauth(token=token)
def list_projects(self):
projects = []
for ws in self._client.workspaces.find_all():
for project in self._client.projects.find_all(workspace=ws['id']):
project = self._client.projects.find_by_id(project['id'])
projects.append({
"id": project['id'],
"name": "{}/{}".format(ws['name'], project['name']),
"description": project['notes'],
"is_private": True,
})
return projects
def list_users(self, project_id):
users = []
for ws in self._client.workspaces.find_all():
for user in self._client.users.find_by_workspace(ws['id'], fields=["id", "name", "email", "photo"]):
users.append({
"id": user["id"],
"full_name": user['name'],
"detected_user": self._get_user(user),
"avatar": user.get('photo', None) and user['photo'].get('image_60x60', None)
})
return users
def _get_user(self, user, default=None):
if not user:
return default
try:
return User.objects.get(email=user['email'])
except User.DoesNotExist:
pass
return default
def import_project(self, project_id, options):
project = self._client.projects.find_by_id(project_id)
taiga_project = self._import_project_data(project, options)
self._import_user_stories_data(taiga_project, project, options)
Timeline.objects.filter(project=taiga_project).delete()
rebuild_timeline(None, None, taiga_project.id)
return taiga_project
def _import_project_data(self, project, options):
users_bindings = options.get('users_bindings', {})
project_template = ProjectTemplate.objects.get(slug=options.get('template', 'scrum'))
project_template.us_statuses = []
project_template.us_statuses.append({
"name": "Open",
"slug": "open",
"is_closed": False,
"is_archived": False,
"color": "#ff8a84",
"wip_limit": None,
"order": 1,
})
project_template.us_statuses.append({
"name": "Closed",
"slug": "closed",
"is_closed": True,
"is_archived": False,
"color": "#669900",
"wip_limit": None,
"order": 2,
})
project_template.default_options["us_status"] = "Open"
project_template.task_statuses = []
project_template.task_statuses.append({
"name": "Open",
"slug": "open",
"is_closed": False,
"color": "#ff8a84",
"order": 1,
})
project_template.task_statuses.append({
"name": "Closed",
"slug": "closed",
"is_closed": True,
"color": "#669900",
"order": 2,
})
project_template.default_options["task_status"] = "Open"
project_template.roles.append({
"name": "Asana",
"slug": "asana",
"computable": False,
"permissions": project_template.roles[0]['permissions'],
"order": 70,
})
tags_colors = []
for tag in self._client.tags.find_by_workspace(project['workspace']['id'], fields=["name", "color"]):
name = tag['name'].lower()
color = tag['color']
tags_colors.append([name, color])
taiga_project = Project.objects.create(
name=options.get('name', None) or project['name'],
description=options.get('description', None) or project['notes'],
owner=self._user,
tags_colors=tags_colors,
creation_template=project_template,
is_private=options.get('is_private', False)
)
for user in self._client.users.find_by_workspace(project['workspace']['id']):
taiga_user = users_bindings.get(user['id'], None)
if taiga_user is None or taiga_user == self._user:
continue
Membership.objects.create(
user=taiga_user,
project=taiga_project,
role=taiga_project.get_roles().get(slug="asana"),
is_admin=False,
invited_by=self._user,
)
UserStoryCustomAttribute.objects.create(
name="Due date",
description="Due date",
type="date",
order=1,
project=taiga_project
)
TaskCustomAttribute.objects.create(
name="Due date",
description="Due date",
type="date",
order=1,
project=taiga_project
)
return taiga_project
def _import_user_stories_data(self, taiga_project, project, options):
users_bindings = options.get('users_bindings', {})
tasks = self._client.tasks.find_by_project(
project['id'],
fields=["parent", "tags", "name", "notes", "tags.name",
"completed", "followers", "modified_at", "created_at",
"project", "due_on"]
)
due_date_field = taiga_project.userstorycustomattributes.first()
for task in tasks:
if task['parent']:
continue
tags = []
for tag in task['tags']:
tags.append(tag['name'].lower())
assigned_to = users_bindings.get(task.get('assignee', {}).get('id', None)) or None
external_reference = None
if options.get('keep_external_reference', False):
external_url = "https://app.asana.com/0/{}/{}".format(
project['id'],
task['id'],
)
external_reference = ["asana", external_url]
us = UserStory.objects.create(
project=taiga_project,
owner=self._user,
assigned_to=assigned_to,
status=taiga_project.us_statuses.get(slug="closed" if task['completed'] else "open"),
kanban_order=task['id'],
sprint_order=task['id'],
backlog_order=task['id'],
subject=task['name'],
description=task.get('notes', ""),
tags=tags,
external_reference=external_reference
)
if task['due_on']:
us.custom_attributes_values.attributes_values = {due_date_field.id: task['due_on']}
us.custom_attributes_values.save()
for follower in task['followers']:
follower_user = users_bindings.get(follower['id'], None)
if follower_user is not None:
us.add_watcher(follower_user)
UserStory.objects.filter(id=us.id).update(
modified_date=task['modified_at'],
created_date=task['created_at']
)
subtasks = self._client.tasks.subtasks(
task['id'],
fields=["parent", "tags", "name", "notes", "tags.name",
"completed", "followers", "modified_at", "created_at",
"due_on"]
)
for subtask in subtasks:
self._import_task_data(taiga_project, us, project, subtask, options)
take_snapshot(us, comment="", user=None, delete=False)
self._import_history(us, task, options)
self._import_attachments(us, task, options)
def _import_task_data(self, taiga_project, us, assana_project, task, options):
users_bindings = options.get('users_bindings', {})
tags = []
for tag in task['tags']:
tags.append(tag['name'].lower())
due_date_field = taiga_project.taskcustomattributes.first()
assigned_to = users_bindings.get(task.get('assignee', {}).get('id', None)) or None
external_reference = None
if options.get('keep_external_reference', False):
external_url = "https://app.asana.com/0/{}/{}".format(
assana_project['id'],
task['id'],
)
external_reference = ["asana", external_url]
taiga_task = Task.objects.create(
project=taiga_project,
user_story=us,
owner=self._user,
assigned_to=assigned_to,
status=taiga_project.task_statuses.get(slug="closed" if task['completed'] else "open"),
us_order=task['id'],
taskboard_order=task['id'],
subject=task['name'],
description=task.get('notes', ""),
tags=tags,
external_reference=external_reference
)
if task['due_on']:
taiga_task.custom_attributes_values.attributes_values = {due_date_field.id: task['due_on']}
taiga_task.custom_attributes_values.save()
for follower in task['followers']:
follower_user = users_bindings.get(follower['id'], None)
if follower_user is not None:
taiga_task.add_watcher(follower_user)
Task.objects.filter(id=taiga_task.id).update(
modified_date=task['modified_at'],
created_date=task['created_at']
)
subtasks = self._client.tasks.subtasks(
task['id'],
fields=["parent", "tags", "name", "notes", "tags.name",
"completed", "followers", "modified_at", "created_at",
"due_on"]
)
for subtask in subtasks:
self._import_task_data(taiga_project, us, assana_project, subtask, options)
take_snapshot(taiga_task, comment="", user=None, delete=False)
self._import_history(taiga_task, task, options)
self._import_attachments(taiga_task, task, options)
def _import_history(self, obj, task, options):
users_bindings = options.get('users_bindings', {})
stories = self._client.stories.find_by_task(task['id'])
for story in stories:
if story['type'] == "comment":
snapshot = take_snapshot(
obj,
comment=story['text'],
user=users_bindings.get(story['created_by']['id'], User(full_name=story['created_by']['name'])),
delete=False
)
HistoryEntry.objects.filter(id=snapshot.id).update(created_at=story['created_at'])
def _import_attachments(self, obj, task, options):
attachments = self._client.attachments.find_by_task(
task['id'],
fields=['name', 'download_url', 'created_at']
)
for attachment in attachments:
data = requests.get(attachment['download_url'])
att = Attachment(
owner=self._user,
project=obj.project,
content_type=ContentType.objects.get_for_model(obj),
object_id=obj.id,
name=attachment['name'],
size=len(data.content),
created_date=attachment['created_at'],
is_deprecated=False,
)
att.attached_file.save(attachment['name'], ContentFile(data.content), save=True)
@classmethod
def get_auth_url(cls, client_id, client_secret, callback_url=None):
client = AsanaClient.oauth(
client_id=client_id,
client_secret=client_secret,
redirect_uri=callback_url
)
(url, state) = client.session.authorization_url()
return url
@classmethod
def get_access_token(cls, code, client_id, client_secret, callback_url=None):
client = AsanaClient.oauth(
client_id=client_id,
client_secret=client_secret,
redirect_uri=callback_url
)
return client.session.fetch_token(code=code)

View File

@ -0,0 +1,56 @@
# -*- 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 .importer import AsanaImporter
logger = logging.getLogger('taiga.importers.asana')
@app.task(bind=True)
def import_project(self, user_id, token, project_id, options):
user = User.object.get(id=user_id)
importer = AsanaImporter(user, token)
try:
project = importer.import_project(project_id, options)
except Exception as e:
# Error
ctx = {
"user": user,
"error_subject": _("Error importing Asana project"),
"error_message": _("Error importing Asana project"),
"project": project_id,
"exception": e
}
email = mail_builder.asana_import_error(admin, ctx)
email.send()
logger.error('Error importing Asana project %s (by %s)', project_id, user, exc_info=sys.exc_info())
else:
ctx = {
"project": project,
"user": user,
}
email = mail_builder.asana_import_success(user, ctx)
email.send()

View File

@ -0,0 +1,8 @@
class InvalidRequest(Exception):
pass
class InvalidAuthResult(Exception):
pass
class FailedRequest(Exception):
pass

View File

@ -0,0 +1,139 @@
# -*- 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.projects.serializers import ProjectSerializer
from taiga.importers import permissions
from taiga.importers import exceptions
from taiga.importers.services import resolve_users_bindings
from .importer import GithubImporter
from . import tasks
class GithubImporterViewSet(viewsets.ViewSet):
permission_classes = (permissions.ImporterPermission,)
@list_route(methods=["POST"])
def list_users(self, request, *args, **kwargs):
self.check_permissions(request, "list_users", None)
token = request.DATA.get('token', None)
project_id = request.DATA.get('project', None)
if not project_id:
raise exc.WrongArguments(_("The project param is needed"))
importer = GithubImporter(request.user, token)
users = importer.list_users(project_id)
for user in users:
if user['detected_user']:
user['user'] = {
'id': user['detected_user'].id,
'full_name': user['detected_user'].get_full_name(),
'gravatar_id': get_user_gravatar_id(user['detected_user']),
'photo': get_user_photo_url(user['detected_user']),
}
del(user['detected_user'])
return response.Ok(users)
@list_route(methods=["POST"])
def list_projects(self, request, *args, **kwargs):
self.check_permissions(request, "list_projects", None)
token = request.DATA.get('token', None)
importer = GithubImporter(request.user, token)
projects = importer.list_projects()
return response.Ok(projects)
@list_route(methods=["POST"])
def import_project(self, request, *args, **kwargs):
self.check_permissions(request, "import_project", None)
token = request.DATA.get('token', None)
project_id = request.DATA.get('project', None)
if not project_id:
raise exc.WrongArguments(_("The project param is needed"))
template = request.DATA.get('template', "scrum")
items_type = "user_stories"
if template == "issues":
items_type = "issues"
template = "scrum"
options = {
"name": request.DATA.get('name', None),
"description": request.DATA.get('description', None),
"template": template,
"type": items_type,
"users_bindings": resolve_users_bindings(request.DATA.get("users_bindings", {})),
"keep_external_reference": request.DATA.get("keep_external_reference", False),
"is_private": request.DATA.get("is_private", False),
}
if settings.CELERY_ENABLED:
task = tasks.import_project.delay(request.user.id, token, project_id, options)
return response.Accepted({"task_id": task.id})
importer = GithubImporter(request.user, token)
project = importer.import_project(project_id, options)
project_data = {
"slug": project.slug,
"my_permissions": ["view_us"],
"is_backlog_activated": project.is_backlog_activated,
"is_kanban_activated": project.is_kanban_activated,
}
return response.Ok(project_data)
@list_route(methods=["GET"])
def auth_url(self, request, *args, **kwargs):
self.check_permissions(request, "auth_url", None)
callback_uri = request.QUERY_PARAMS.get('uri')
url = GithubImporter.get_auth_url(
settings.IMPORTERS.get('github', {}).get('client_id', None),
callback_uri
)
return response.Ok({"url": url})
@list_route(methods=["POST"])
def authorize(self, request, *args, **kwargs):
self.check_permissions(request, "authorize", None)
code = request.DATA.get('code', None)
if code is None:
raise exc.BadRequest(_("Code param needed"))
try:
token = GithubImporter.get_access_token(
settings.IMPORTERS.get('github', {}).get('client_id', None),
settings.IMPORTERS.get('github', {}).get('client_secret', None),
code
)
return response.Ok({
"token": token
})
except exceptions.InvalidAuthResult:
raise exc.BadRequest(_("Invalid auth data"))
except exceptions.FailedRequest:
raise exc.BadRequest(_("Third party service failing"))

View File

@ -0,0 +1,610 @@
import requests
from urllib.parse import parse_qsl
from django.core.files.base import ContentFile
from taiga.projects.models import Project, ProjectTemplate, Membership
from taiga.projects.references.models import recalc_reference_counter
from taiga.projects.userstories.models import UserStory
from taiga.projects.issues.models import Issue
from taiga.projects.milestones.models import Milestone
from taiga.projects.history.services import take_snapshot
from taiga.projects.history.services import (make_diff_from_dicts,
make_diff_values,
make_key_from_model_object,
get_typename_for_model_class,
FrozenDiff)
from taiga.projects.history.models import HistoryEntry
from taiga.projects.history.choices import HistoryType
from taiga.timeline.rebuilder import rebuild_timeline
from taiga.timeline.models import Timeline
from taiga.users.models import User, AuthData
from taiga.importers.exceptions import InvalidAuthResult
class GithubClient:
def __init__(self, token):
self.api_url = "https://api.github.com/{}"
self.token = token
def get(self, uri_path, query_params=None):
headers = {
"Content-Type": "application/json",
"X-GitHub-Media-Type": "github.v3"
}
if self.token:
headers['Authorization'] = 'token {}'.format(self.token)
if query_params is None:
query_params = {}
if uri_path[0] == '/':
uri_path = uri_path[1:]
url = self.api_url.format(uri_path)
response = requests.get(url, params=query_params, headers=headers)
if response.status_code == 401:
raise Exception("Unauthorized: %s at %s" % (response.text, url), response)
if response.status_code != 200:
raise Exception("Resource Unavailable: %s at %s" % (response.text, url), response)
return response.json()
class GithubImporter:
def __init__(self, user, token, import_closed_data=False):
self._import_closed_data = import_closed_data
self._user = user
self._client = GithubClient(token)
self._me = self._client.get("/user")
def list_projects(self):
projects = []
page = 1
while True:
repos = self._client.get("/user/repos", {
"sort": "full_name",
"page": page,
"per_page": 100
})
page += 1
for repo in repos:
projects.append({
"id": repo['full_name'],
"name": repo['full_name'],
"description": repo['description'],
"is_private": repo['private'],
})
if len(repos) < 100:
break
return projects
def list_users(self, project_full_name):
collaborators = self._client.get("/repos/{}/collaborators".format(project_full_name))
collaborators = [self._client.get("/users/{}".format(u['login'])) for u in collaborators]
return [{"id": u['id'],
"username": u['login'],
"full_name": u.get('name', u['login']),
"avatar": u.get('avatar_url', None),
"detected_user": self._get_user(u) } for u in collaborators]
def _get_user(self, user, default=None):
if not user:
return default
try:
return AuthData.objects.get(key="github", value=user['id']).user
except AuthData.DoesNotExist:
pass
try:
return User.objects.get(email=user.get('email', "not-valid"))
except User.DoesNotExist:
pass
return default
def import_project(self, project_full_name, options={"keep_external_reference": False, "template": "kanban", "type": "user_stories"}):
repo = self._client.get('/repos/{}'.format(project_full_name))
project = self._import_project_data(repo, options)
if options.get('type', None) == "user_stories":
self._import_user_stories_data(project, repo, options)
elif options.get('type', None) == "issues":
self._import_issues_data(project, repo, options)
self._import_comments(project, repo, options)
self._import_history(project, repo, options)
Timeline.objects.filter(project=project).delete()
rebuild_timeline(None, None, project.id)
recalc_reference_counter(project)
return project
def _import_project_data(self, repo, options):
users_bindings = options.get('users_bindings', {})
project_template = ProjectTemplate.objects.get(slug=options['template'])
if options['type'] == "user_stories":
project_template.us_statuses = []
project_template.us_statuses.append({
"name": "Open",
"slug": "open",
"is_closed": False,
"is_archived": False,
"color": "#ff8a84",
"wip_limit": None,
"order": 1,
})
project_template.us_statuses.append({
"name": "Closed",
"slug": "closed",
"is_closed": True,
"is_archived": False,
"color": "#669900",
"wip_limit": None,
"order": 2,
})
project_template.default_options["us_status"] = "Open"
elif options['type'] == "issues":
project_template.issue_statuses = []
project_template.issue_statuses.append({
"name": "Open",
"slug": "open",
"is_closed": False,
"color": "#ff8a84",
"order": 1,
})
project_template.issue_statuses.append({
"name": "Closed",
"slug": "closed",
"is_closed": True,
"color": "#669900",
"order": 2,
})
project_template.default_options["issue_status"] = "Open"
project_template.roles.append({
"name": "Github",
"slug": "github",
"computable": False,
"permissions": project_template.roles[0]['permissions'],
"order": 70,
})
tags_colors = []
for label in self._client.get("/repos/{}/labels".format(repo['full_name'])):
name = label['name'].lower()
color = "#{}".format(label['color'])
tags_colors.append([name, color])
project = Project.objects.create(
name=options.get('name', None) or repo['full_name'],
description=options.get('description', None) or repo['description'],
owner=self._user,
tags_colors=tags_colors,
creation_template=project_template,
is_private=options.get('is_private', False),
)
if 'organization' in repo and repo['organization'].get('avatar_url', None):
data = requests.get(repo['organization']['avatar_url'])
project.logo.save("logo.png", ContentFile(data.content), save=True)
for user in self._client.get("/repos/{}/collaborators".format(repo['full_name'])):
taiga_user = users_bindings.get(user['id'], None)
if taiga_user is None or taiga_user == self._user:
continue
Membership.objects.create(
user=taiga_user,
project=project,
role=project.get_roles().get(slug="github"),
is_admin=False,
invited_by=self._user,
)
for milestone in self._client.get("/repos/{}/milestones".format(repo['full_name'])):
taiga_milestone = Milestone.objects.create(
name=milestone['title'],
owner=users_bindings.get(milestone.get('creator', {}).get('id', None), self._user),
project=project,
estimated_start=milestone['created_at'][:10],
estimated_finish=milestone['due_on'][:10],
)
Milestone.objects.filter(id=taiga_milestone.id).update(
created_date=milestone['created_at'],
modified_date=milestone['updated_at'],
)
return project
def _import_user_stories_data(self, project, repo, options):
users_bindings = options.get('users_bindings', {})
page = 1
while True:
issues = self._client.get("/repos/{}/issues".format(repo['full_name']), {
"state": "all",
"sort": "created",
"direction": "asc",
"page": page,
"per_page": 100
})
page += 1
for issue in issues:
tags = []
for label in issue['labels']:
tags.append(label['name'].lower())
assigned_to = users_bindings.get(issue['assignee']['id'] if issue['assignee'] else None, None)
external_reference = None
if options.get('keep_external_reference', False):
external_reference = ["github", issue['html_url']]
us = UserStory.objects.create(
ref=issue['number'],
project=project,
owner=users_bindings.get(issue['user']['id'], self._user),
milestone=project.milestones.get(name=issue['milestone']['title']) if issue['milestone'] else None,
assigned_to=assigned_to,
status=project.us_statuses.get(slug=issue['state']),
kanban_order=issue['number'],
sprint_order=issue['number'],
backlog_order=issue['number'],
subject=issue['title'],
description=issue.get("body", "") or "",
tags=tags,
external_reference=external_reference,
modified_date=issue['updated_at'],
created_date=issue['created_at'],
)
assignees = issue.get('assignees', [])
if len(assignees) > 1:
for assignee in assignees:
if assignee['id'] != issue.get('assignee', {}).get('id', None):
assignee_user = users_bindings.get(assignee['id'], None)
if assignee_user is not None:
us.add_watcher(assignee_user)
UserStory.objects.filter(id=us.id).update(
ref=issue['number'],
modified_date=issue['updated_at'],
created_date=issue['created_at']
)
take_snapshot(us, comment="", user=None, delete=False)
if len(issues) < 100:
break
def _import_issues_data(self, project, repo, options):
users_bindings = options.get('users_bindings', {})
page = 1
while True:
issues = self._client.get("/repos/{}/issues".format(repo['full_name']), {
"state": "all",
"sort": "created",
"direction": "asc",
"page": page,
"per_page": 100
})
page += 1
for issue in issues:
tags = []
for label in issue['labels']:
tags.append(label['name'].lower())
assigned_to = users_bindings.get(issue['assignee']['id'] if issue['assignee'] else None, None)
external_reference = None
if options.get('keep_external_reference', False):
external_reference = ["github", issue['html_url']]
taiga_issue = Issue.objects.create(
ref=issue['number'],
project=project,
owner=users_bindings.get(issue['user']['id'], self._user),
assigned_to=assigned_to,
status=project.issue_statuses.get(slug=issue['state']),
subject=issue['title'],
description=issue.get('body', "") or "",
tags=tags,
external_reference=external_reference,
modified_date=issue['updated_at'],
created_date=issue['created_at'],
)
assignees = issue.get('assignees', [])
if len(assignees) > 1:
for assignee in assignees:
if assignee['id'] != issue.get('assignee', {}).get('id', None):
assignee_user = users_bindings.get(assignee['id'], None)
if assignee_user is not None:
taiga_issue.add_watcher(assignee_user)
Issue.objects.filter(id=taiga_issue.id).update(
ref=issue['number'],
modified_date=issue['updated_at'],
created_date=issue['created_at']
)
take_snapshot(taiga_issue, comment="", user=None, delete=False)
if len(issues) < 100:
break
def _import_comments(self, project, repo, options):
users_bindings = options.get('users_bindings', {})
page = 1
while True:
comments = self._client.get("/repos/{}/issues/comments".format(repo['full_name']), {
"page": page,
"per_page": 100
})
page += 1
for comment in comments:
issue_id = comment['issue_url'].split("/")[-1]
if options.get('type', None) == "user_stories":
obj = UserStory.objects.get(project=project, ref=issue_id)
elif options.get('type', None) == "issues":
obj = Issue.objects.get(project=project, ref=issue_id)
snapshot = take_snapshot(
obj,
comment=comment['body'],
user=users_bindings.get(comment['user']['id'], User(full_name=comment['user'].get('name', None) or comment['user']['login'])),
delete=False
)
HistoryEntry.objects.filter(id=snapshot.id).update(created_at=comment['created_at'])
if len(comments) < 100:
break
def _import_history(self, project, repo, options):
cumulative_data = {}
page = 1
all_events = []
while True:
events = self._client.get("/repos/{}/issues/events".format(repo['full_name']), {
"page": page,
"per_page": 100
})
page += 1
all_events = all_events + events
if len(events) < 100:
break
for event in sorted(all_events, key=lambda x: x['id']):
if options.get('type', None) == "user_stories":
obj = UserStory.objects.get(project=project, ref=event['issue']['number'])
elif options.get('type', None) == "issues":
obj = Issue.objects.get(project=project, ref=event['issue']['number'])
if event['issue']['number'] in cumulative_data:
obj_cumulative_data = cumulative_data[event['issue']['number']]
else:
obj_cumulative_data = {
"tags": set(),
"assigned_to": None,
"assigned_to_github_id": None,
"assigned_to_name": None,
"milestone": None,
}
cumulative_data[event['issue']['number']] = obj_cumulative_data
self._import_event(obj, event, options, obj_cumulative_data)
def _import_event(self, obj, event, options, cumulative_data):
typename = get_typename_for_model_class(type(obj))
key = make_key_from_model_object(obj)
event_data = self._transform_event_data(obj, event, options, cumulative_data)
if event_data is None:
return
change_old = event_data['change_old']
change_new = event_data['change_new']
user = event_data['user']
diff = make_diff_from_dicts(change_old, change_new)
fdiff = FrozenDiff(key, diff, {})
values = make_diff_values(typename, fdiff)
values.update(event_data['update_values'])
entry = HistoryEntry.objects.create(
user=user,
project_id=obj.project.id,
key=key,
type=HistoryType.change,
snapshot=None,
diff=fdiff.diff,
values=values,
comment="",
comment_html="",
is_hidden=False,
is_snapshot=False,
)
HistoryEntry.objects.filter(id=entry.id).update(created_at=event['created_at'])
return HistoryEntry.objects.get(id=entry.id)
def _transform_event_data(self, obj, event, options, cumulative_data):
users_bindings = options.get('users_bindings', {})
ignored_events = ["committed", "cross-referenced", "head_ref_deleted",
"head_ref_restored", "locked", "unlocked", "merged",
"referenced", "mentioned", "subscribed",
"unsubscribed"]
if event['event'] in ignored_events:
return None
user = {"pk": None, "name": event['actor'].get('name', event['actor']['login'])}
taiga_user = users_bindings.get(event['actor']['id'], None) if event['actor'] else None
if taiga_user:
user = {"pk": taiga_user.id, "name": taiga_user.get_full_name()}
result = {
"change_old": {},
"change_new": {},
"user": user,
"update_values": {},
}
if event['event'] == "renamed":
result['change_old']["subject"] = event['rename']['from']
result['change_new']["subject"] = event['rename']['to']
elif event['event'] == "reopened":
if isinstance(obj, Issue):
result['change_old']["status"] = obj.project.issue_statuses.get(name='Closed').id
result['change_new']["status"] = obj.project.issue_statuses.get(name='Open').id
elif isinstance(obj, UserStory):
result['change_old']["status"] = obj.project.us_statuses.get(name='Closed').id
result['change_new']["status"] = obj.project.us_statuses.get(name='Open').id
elif event['event'] == "closed":
if isinstance(obj, Issue):
result['change_old']["status"] = obj.project.issue_statuses.get(name='Open').id
result['change_new']["status"] = obj.project.issue_statuses.get(name='Closed').id
elif isinstance(obj, UserStory):
result['change_old']["status"] = obj.project.us_statuses.get(name='Open').id
result['change_new']["status"] = obj.project.us_statuses.get(name='Closed').id
elif event['event'] == "assigned":
AssignedEventHandler(result, cumulative_data, users_bindings).handle(event)
elif event['event'] == "unassigned":
UnassignedEventHandler(result, cumulative_data, users_bindings).handle(event)
elif event['event'] == "demilestoned":
if isinstance(obj, UserStory):
try:
result['change_old']["milestone"] = obj.project.milestones.get(name=event['milestone']['title']).id
except Milestone.DoesNotExist:
result['change_old']["milestone"] = 0
result['update_values'] = {"milestone": {"0": event['milestone']['title']}}
result['change_new']["milestone"] = None
cumulative_data['milestone'] = None
elif event['event'] == "milestoned":
if isinstance(obj, UserStory):
result['update_values']["milestone"] = {}
if cumulative_data['milestone'] is not None:
result['update_values']['milestone'][str(cumulative_data['milestone'])] = cumulative_data['milestone_name']
result['change_old']["milestone"] = cumulative_data['milestone']
try:
taiga_milestone = obj.project.milestones.get(name=event['milestone']['title'])
cumulative_data["milestone"] = taiga_milestone.id
cumulative_data['milestone_name'] = taiga_milestone.name
except Milestone.DoesNotExist:
if cumulative_data['milestone'] == 0:
cumulative_data['milestone'] = -1
else:
cumulative_data['milestone'] = 0
cumulative_data['milestone_name'] = event['milestone']['title']
result['change_new']["milestone"] = cumulative_data['milestone']
result['update_values']['milestone'][str(cumulative_data['milestone'])] = cumulative_data['milestone_name']
elif event['event'] == "labeled":
result['change_old']["tags"] = list(cumulative_data['tags'])
cumulative_data['tags'].add(event['label']['name'].lower())
result['change_new']["tags"] = list(cumulative_data['tags'])
elif event['event'] == "unlabeled":
result['change_old']["tags"] = list(cumulative_data['tags'])
if event['label']['name'].lower() in cumulative_data['tags']:
cumulative_data['tags'].remove(event['label']['name'].lower())
result['change_new']["tags"] = list(cumulative_data['tags'])
return result
@classmethod
def get_auth_url(cls, client_id, callback_uri=None):
if callback_uri is None:
return "https://github.com/login/oauth/authorize?client_id={}&scope=user,repo".format(client_id)
return "https://github.com/login/oauth/authorize?client_id={}&scope=user,repo&redirect_uri={}".format(client_id, callback_uri)
@classmethod
def get_access_token(cls, client_id, client_secret, code):
try:
result = requests.post("https://github.com/login/oauth/access_token", {
"client_id": client_id,
"client_secret": client_secret,
"code": code,
})
except Exception:
raise FailedRequest()
if result.status_code > 299:
raise InvalidAuthResult()
else:
try:
return dict(parse_qsl(result.content))[b'access_token'].decode('utf-8')
except:
raise InvalidAuthResult()
class AssignedEventHandler:
def __init__(self, result, cumulative_data, users_bindings):
self.result = result
self.cumulative_data = cumulative_data
self.users_bindings = users_bindings
def handle(self, event):
if self.cumulative_data['assigned_to_github_id'] is None:
self.result['update_values']["users"] = {}
self.generate_change_old(event)
self.generate_update_values_from_cumulative_data(event)
user = self.users_bindings.get(event['assignee']['id'], None)
self.generate_change_new(event, user)
self.update_cumulative_data(event, user)
self.generate_update_values_from_cumulative_data(event)
def generate_change_old(self, event):
self.result['change_old']["assigned_to"] = self.cumulative_data['assigned_to']
def generate_update_values_from_cumulative_data(self, event):
if self.cumulative_data['assigned_to_name'] is not None:
self.result['update_values']["users"][str(self.cumulative_data['assigned_to'])] = self.cumulative_data['assigned_to_name']
def generate_change_new(self, event, user):
if user is None:
self.result['change_new']["assigned_to"] = 0
else:
self.result['change_new']["assigned_to"] = user.id
def update_cumulative_data(self, event, user):
self.cumulative_data['assigned_to_github_id'] = event['assignee']['id']
if user is None:
self.cumulative_data['assigned_to'] = 0
self.cumulative_data['assigned_to_name'] = event['assignee']['login']
else:
self.cumulative_data['assigned_to'] = user.id
self.cumulative_data['assigned_to_name'] = user.get_full_name()
class UnassignedEventHandler:
def __init__(self, result, cumulative_data, users_bindings):
self.result = result
self.cumulative_data = cumulative_data
self.users_bindings = users_bindings
def handle(self, event):
if self.cumulative_data['assigned_to_github_id'] == event['assignee']['id']:
self.result['update_values']["users"] = {}
self.generate_change_old(event)
self.generate_update_values_from_cumulative_data(event)
self.generate_change_new(event)
self.update_cumulative_data(event)
self.generate_update_values_from_cumulative_data(event)
def generate_change_old(self, event):
self.result['change_old']["assigned_to"] = self.cumulative_data['assigned_to']
def generate_update_values_from_cumulative_data(self, event):
if self.cumulative_data['assigned_to_name'] is not None:
self.result['update_values']["users"][str(self.cumulative_data['assigned_to'])] = self.cumulative_data['assigned_to_name']
def generate_change_new(self, event):
self.result['change_new']["assigned_to"] = None
def update_cumulative_data(self, event):
self.cumulative_data['assigned_to_github_id'] = None
self.cumulative_data['assigned_to'] = None
self.cumulative_data['assigned_to_name'] = None

View File

@ -0,0 +1,56 @@
# -*- 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.celery import app
from taiga.users.models import User
from .importer import GithubImporter
logger = logging.getLogger('taiga.importers.github')
@app.task(bind=True)
def import_project(self, user_id, token, project_id, options):
user = User.object.get(id=user_id)
importer = GithubImporter(user, token)
try:
project = importer.import_project(project_id, options)
except Exception as e:
# Error
ctx = {
"user": user,
"error_subject": _("Error importing GitHub project"),
"error_message": _("Error importing GitHub project"),
"project": project_id,
"exception": e
}
email = mail_builder.github_import_error(admin, ctx)
email.send()
logger.error('Error importing GitHub project %s (by %s)', project_id, user, exc_info=sys.exc_info())
else:
ctx = {
"project": project,
"user": user,
}
email = mail_builder.github_import_success(user, ctx)
email.send()

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

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

@ -0,0 +1,230 @@
# -*- 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 taiga.importers.services import resolve_users_bindings
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.IMPORTERS.get('jira', {}).get('cert', None),
"consumer_key": settings.IMPORTERS.get('jira', {}).get('consumer_key', None)
}
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": resolve_users_bindings(request.DATA.get("users_bindings", {})),
"keep_external_reference": request.DATA.get("keep_external_reference", False),
"is_private": request.DATA.get("is_private", False),
}
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.IMPORTERS.get('jira', {}).get('consumer_key', None),
settings.IMPORTERS.get('jira', {}).get('cert', None),
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.IMPORTERS.get('jira', {}).get('consumer_key', None),
settings.IMPORTERS.get('jira', {}).get('cert', None),
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,749 @@
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'],
"avatar": user_data.get('avatarUrls', None) and user_data['avatarUrls'].get('48x48', None),
})
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

View File

@ -0,0 +1,110 @@
# -*- 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.conf import settings
from django.db.models import Q
from taiga.importers.asana.importer import AsanaImporter
from taiga.users.models import User, AuthData
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('--project-id', dest="project_id", type=str,
help='Project ID or full name (ex: taigaio/taiga-back)')
parser.add_argument('--template', dest='template', default="kanban",
help='template to use: scrum or kanban (default kanban)')
parser.add_argument('--type', dest='type', default="user_stories",
help='type of object to use: user_stories or issues (default user_stories)')
parser.add_argument('--ask-for-users', dest='ask_for_users', const=True,
action="store_const", default=False,
help='Import closed data')
parser.add_argument('--keep-external-reference', dest='keep_external_reference', const=True,
action="store_const", default=False,
help='Store external reference of imported data')
def handle(self, *args, **options):
admin = User.objects.get(username="admin")
if options.get('token', None):
token = json.loads(options.get('token'))
else:
url = AsanaImporter.get_auth_url(
settings.IMPORTERS.get('asana', {}).get('app_id', None),
settings.IMPORTERS.get('asana', {}).get('app_secret', None),
settings.IMPORTERS.get('asana', {}).get('callback_url', None)
)
print("Go to here and come with your code (in the redirected url): {}".format(url))
code = input("Code: ")
access_data = AsanaImporter.get_access_token(
code,
settings.IMPORTERS.get('asana', {}).get('app_id', None),
settings.IMPORTERS.get('asana', {}).get('app_secret', None),
settings.IMPORTERS.get('asana', {}).get('callback_url', None)
)
token = access_data
importer = AsanaImporter(admin, token)
if options.get('project_id', None):
project_id = options.get('project_id')
else:
print("Select the project to import:")
for project in importer.list_projects():
print("- {}: {}".format(project['id'], project['name']))
project_id = input("Project id: ")
users_bindings = {}
if options.get('ask_for_users', None):
print("Add the username or email for next asana users:")
for user in importer.list_users(project_id):
while True:
if user['detected_user'] is not None:
print("User automatically detected: {} as {}".format(user['full_name'], user['detected_user']))
users_bindings[user['id']] = user['detected_user']
break
if not options.get('ask_for_users', False):
break
username_or_email = input("{}: ".format(user['full_name'] or user['username']))
if username_or_email == "":
break
try:
users_bindings[user['id']] = User.objects.get(Q(username=username_or_email) | Q(email=username_or_email))
break
except User.DoesNotExist:
print("ERROR: Invalid username or email")
options = {
"template": options.get('template'),
"type": options.get('type'),
"users_bindings": users_bindings,
"keep_external_reference": options.get('keep_external_reference')
}
importer.import_project(project_id, options)

View File

@ -0,0 +1,106 @@
# -*- 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.conf import settings
from django.db.models import Q
from taiga.importers.github.importer import GithubImporter
from taiga.users.models import User, AuthData
from taiga.projects.services import projects as service
import unittest.mock
import timeit
class Command(BaseCommand):
def add_arguments(self, parser):
parser.add_argument('--token', dest="token", type=str,
help='Auth token')
parser.add_argument('--project-id', dest="project_id", type=str,
help='Project ID or full name (ex: taigaio/taiga-back)')
parser.add_argument('--template', dest='template', default="kanban",
help='template to use: scrum or kanban (default kanban)')
parser.add_argument('--type', dest='type', default="user_stories",
help='type of object to use: user_stories or issues (default user_stories)')
parser.add_argument('--ask-for-users', dest='ask_for_users', const=True,
action="store_const", default=False,
help='Import closed data')
parser.add_argument('--keep-external-reference', dest='keep_external_reference', const=True,
action="store_const", default=False,
help='Store external reference of imported data')
def handle(self, *args, **options):
admin = User.objects.get(username="admin")
if options.get('token', None):
token = options.get('token')
else:
url = GithubImporter.get_auth_url(
settings.IMPORTERS.get('github', {}).get('client_id', None)
)
print("Go to here and come with your code (in the redirected url): {}".format(url))
code = input("Code: ")
access_data = GithubImporter.get_access_token(
settings.IMPORTERS.get('github', {}).get('client_id', None)
settings.IMPORTERS.get('github', {}).get('client_secret', None)
code
)
token = access_data
importer = GithubImporter(admin, token)
if options.get('project_id', None):
project_id = options.get('project_id')
else:
print("Select the project to import:")
for project in importer.list_projects():
print("- {}: {}".format(project['id'], project['name']))
project_id = input("Project id: ")
users_bindings = {}
if options.get('ask_for_users', None):
print("Add the username or email for next github users:")
for user in importer.list_users(project_id):
while True:
if user['detected_user'] is not None:
print("User automatically detected: {} as {}".format(user['full_name'], user['detected_user']))
users_bindings[user['id']] = user['detected_user']
break
if not options.get('ask_for_users', False):
break
username_or_email = input("{}: ".format(user['full_name'] or user['username']))
if username_or_email == "":
break
try:
users_bindings[user['id']] = User.objects.get(Q(username=username_or_email) | Q(email=username_or_email))
break
except User.DoesNotExist:
print("ERROR: Invalid username or email")
options = {
"template": options.get('template'),
"type": options.get('type'),
"users_bindings": users_bindings,
"keep_external_reference": options.get('keep_external_reference')
}
importer.import_project(project_id, options)

View File

@ -0,0 +1,158 @@
# -*- 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.IMPORTERS.get('jira', {}).get('consumer_key', None),
settings.IMPORTERS.get('jira', {}).get('cert', None),
True
)
print(url)
code = input("Go to the url and get back the code")
token = JiraNormalImporter.get_access_token(
server,
settings.IMPORTERS.get('jira', {}).get('consumer_key', None),
settings.IMPORTERS.get('jira', {}).get('cert', None),
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

@ -0,0 +1,93 @@
# -*- 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 taiga.importers.pivotal import PivotalImporter
from taiga.users.models import User
from taiga.projects.services import projects as service
import unittest.mock
import timeit
class Command(BaseCommand):
def add_arguments(self, parser):
parser.add_argument('--token', dest="token", type=str,
help='Auth token')
parser.add_argument('--project-id', dest="project_id", type=str,
help='Project ID or full name (ex: taigaio/taiga-back)')
parser.add_argument('--template', dest='template', default="scrum",
help='template to use: scrum or scrum (default scrum)')
parser.add_argument('--ask-for-users', dest='ask_for_users', const=True,
action="store_const", default=False,
help='Import closed data')
parser.add_argument('--closed-data', dest='closed_data', const=True,
action="store_const", default=False,
help='Import closed data')
parser.add_argument('--keep-external-reference', dest='keep_external_reference', const=True,
action="store_const", default=False,
help='Store external reference of imported data')
def handle(self, *args, **options):
admin = User.objects.get(username="admin")
if options.get('token', None):
token = options.get('token')
else:
print("You need a user token")
return
importer = PivotalImporter(admin, token)
if options.get('project_id', None):
project_id = options.get('project_id')
else:
print("Select the project to import:")
for project in importer.list_projects():
print("- {}: {}".format(project['project_id'], project['project_name']))
project_id = input("Project id: ")
users_bindings = {}
if options.get('ask_for_users', None):
print("Add the username or email for next pivotal users:")
for user in importer.list_users(project_id):
try:
users_bindings[user['id']] = User.objects.get(Q(email=user['person'].get('email', "not-valid")))
break
except User.DoesNotExist:
pass
while True:
username_or_email = input("{}: ".format(user['person']['name']))
if username_or_email == "":
break
try:
users_bindings[user['id']] = User.objects.get(Q(username=username_or_email) | Q(email=username_or_email))
break
except User.DoesNotExist:
print("ERROR: Invalid username or email")
options = {
"template": options.get('template'),
"import_closed_data": options.get("closed_data", False),
"users_bindings": users_bindings,
"keep_external_reference": options.get('keep_external_reference')
}
importer.import_project(project_id, options)

View File

@ -0,0 +1,89 @@
# -*- 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 taiga.importers.trello.importer import TrelloImporter
from taiga.users.models import User
from taiga.projects.services import projects as service
import unittest.mock
import timeit
class Command(BaseCommand):
def add_arguments(self, parser):
parser.add_argument('--token', dest="token", type=str,
help='Auth token')
parser.add_argument('--project-id', dest="project_id", type=str,
help='Project ID or full name (ex: taigaio/taiga-back)')
parser.add_argument('--template', dest='template', default="kanban",
help='template to use: scrum or kanban (default kanban)')
parser.add_argument('--ask-for-users', dest='ask_for_users', const=True,
action="store_const", default=False,
help='Import closed data')
parser.add_argument('--closed-data', dest='closed_data', const=True,
action="store_const", default=False,
help='Import closed data')
parser.add_argument('--keep-external-reference', dest='keep_external_reference', const=True,
action="store_const", default=False,
help='Store external reference of imported data')
def handle(self, *args, **options):
admin = User.objects.get(username="admin")
if options.get('token', None):
token = options.get('token')
else:
(oauth_token, oauth_token_secret, url) = TrelloImporter.get_auth_url()
print("Go to here and come with your token: {}".format(url))
oauth_verifier = input("Code: ")
access_data = TrelloImporter.get_access_token(oauth_token, oauth_token_secret, oauth_verifier)
token = access_data['oauth_token']
print("Access token: {}".format(token))
importer = TrelloImporter(admin, token)
if options.get('project_id', None):
project_id = options.get('project_id')
else:
print("Select the project to import:")
for project in importer.list_projects():
print("- {}: {}".format(project['id'], project['name']))
project_id = input("Project id: ")
users_bindings = {}
if options.get('ask_for_users', None):
print("Add the username or email for next trello users:")
for user in importer.list_users(project_id):
while True:
username_or_email = input("{}: ".format(user['fullName']))
if username_or_email == "":
break
try:
users_bindings[user['id']] = User.objects.get(Q(username=username_or_email) | Q(email=username_or_email))
break
except User.DoesNotExist:
print("ERROR: Invalid username or email")
options = {
"template": options.get('template'),
"import_closed_data": options.get("closed_data", False),
"users_bindings": users_bindings,
"keep_external_reference": options.get('keep_external_reference')
}
importer.import_project(project_id, options)

View File

@ -0,0 +1,32 @@
# -*- 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 taiga.base.api.permissions import TaigaResourcePermission, AllowAny, IsAuthenticated
from taiga.base.api.permissions import IsSuperUser, HasProjectPerm, IsProjectAdmin
from taiga.permissions.permissions import CommentAndOrUpdatePerm
class ImporterPermission(TaigaResourcePermission):
enought_perms = IsAuthenticated()
global_perms = None
auth_url_perms = IsAuthenticated()
authorize_perms = IsAuthenticated()
list_users_perms = IsAuthenticated()
list_projects_perms = IsAuthenticated()
import_project_perms = IsAuthenticated()

View File

@ -0,0 +1,143 @@
# -*- 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.projects.serializers import ProjectSerializer
from taiga.importers import permissions
from .importer import PivotalImporter
from . import tasks
class PivotalImporterViewSet(viewsets.ViewSet):
permission_classes = (permissions.ImporterPermission,)
@list_route(methods=["POST"])
def list_users(self, request, *args, **kwargs):
self.check_permissions(request, "list_users", None)
token = request.DATA.get('token', None)
project_id = request.DATA.get('project', None)
if not project_id:
raise exc.WrongArguments(_("The project param is needed"))
importer = PivotalImporter(request.user, token)
users = importer.list_users(project_id)
for user in users:
user['user'] = None
if not user['email']:
continue
try:
taiga_user = User.objects.get(email=user['email'])
except User.DoesNotExist:
continue
user['user'] = {
'id': taiga_user.id,
'full_name': taiga_user.get_full_name(),
'gravatar_id': get_user_gravatar_id(taiga_user),
'photo': get_user_photo_url(taiga_user),
}
return response.Ok(users)
@list_route(methods=["POST"])
def list_projects(self, request, *args, **kwargs):
self.check_permissions(request, "list_projects", None)
token = request.DATA.get('token', None)
importer = PivotalImporter(request.user, token)
projects = importer.list_projects()
return response.Ok(projects)
@list_route(methods=["POST"])
def import_project(self, request, *args, **kwargs):
self.check_permissions(request, "import_project", None)
token = request.DATA.get('token', None)
project_id = request.DATA.get('project', None)
if not project_id:
raise exc.WrongArguments(_("The project param is needed"))
options = {
"template": request.DATA.get('template', "kanban"),
"users_bindings": request.DATA.get("users_bindings", {}),
"keep_external_reference": request.DATA.get("keep_external_reference", False),
"is_private": request.DATA.get("is_private", False),
}
if settings.CELERY_ENABLED:
task = tasks.import_project.delay(request.user.id, token, project_id, options)
return response.Accepted({"pivotal_import_id": task.id})
importer = PivotalImporter(request.user, token)
project = importer.import_project(project_id, options)
project_data = {
"slug": project.slug,
"my_permissions": ["view_us"],
"is_backlog_activated": project.is_backlog_activated,
"is_kanban_activated": project.is_kanban_activated,
}
return response.Ok(project_data)
@list_route(methods=["GET"])
def auth_url(self, request, *args, **kwargs):
self.check_permissions(request, "auth_url", None)
(oauth_token, oauth_secret, url) = PivotalImporter.get_auth_url()
(auth_data, created) = AuthData.objects.get_or_create(
user=request.user,
key="pivotal-oauth",
defaults={
"value": "",
"extra": {},
}
)
auth_data.extra = {
"oauth_token": oauth_token,
"oauth_secret": oauth_secret,
}
auth_data.save()
return response.Ok({"url": url})
@list_route(methods=["POST"])
def authorize(self, request, *args, **kwargs):
self.check_permissions(request, "authorize", None)
try:
oauth_data = request.user.auth_data.get(key="pivotal-oauth")
oauth_token = oauth_data.extra['oauth_token']
oauth_secret = oauth_data.extra['oauth_secret']
oauth_verifier = request.DATA.get('code')
oauth_data.delete()
pivotal_token = PivotalImporter.get_access_token(oauth_token, oauth_secret, oauth_verifier)['oauth_token']
except Exception as e:
raise exc.WrongArguments(_("Invalid or expired auth token"))
return response.Ok({
"token": pivotal_token
})

View File

@ -0,0 +1,702 @@
from django.core.files.base import ContentFile
from django.contrib.contenttypes.models import ContentType
import requests
from taiga.users.models import User
from taiga.projects.references.models import recalc_reference_counter
from taiga.projects.models import Project, ProjectTemplate, Membership, Points
from taiga.projects.userstories.models import UserStory, RolePoints
from taiga.projects.tasks.models import Task
from taiga.projects.milestones.models import Milestone
from taiga.projects.epics.models import Epic, RelatedUserStory
from taiga.projects.attachments.models import Attachment
from taiga.projects.history.services import take_snapshot
from taiga.projects.history.services import (make_diff_from_dicts,
make_diff_values,
make_key_from_model_object,
get_typename_for_model_class,
FrozenDiff)
from taiga.projects.history.models import HistoryEntry
from taiga.projects.history.choices import HistoryType
from taiga.projects.custom_attributes.models import UserStoryCustomAttribute
from taiga.mdrender.service import render as mdrender
from taiga.timeline.rebuilder import rebuild_timeline
from taiga.timeline.models import Timeline
class PivotalClient:
def __init__(self, token):
self.api_url = "https://www.pivotaltracker.com/services/v5/{}"
self.token = token
self.me = self.get('/me')
def get(self, uri_path, query_params=None):
headers = {
'X-TrackerToken': self.token
}
if query_params is None:
query_params = {}
if uri_path[0] == '/':
uri_path = uri_path[1:]
url = self.api_url.format(uri_path)
response = requests.get(url, params=query_params, headers=headers)
if response.status_code == 401:
raise Exception("Unauthorized: %s at %s" % (response.text, url), response)
if response.status_code != 200:
raise Exception("Resource Unavailable: %s at %s" % (response.text, url), response)
return response.json()
def get_attachment(self, attachment_id):
headers = {
'X-TrackerToken': self.token
}
url = "https://www.pivotaltracker.com/file_attachments/{}/download".format(attachment_id)
response = requests.get(url, headers=headers)
return response.content
class PivotalImporter:
def __init__(self, user, token):
self._user = user
self._client = PivotalClient(token=token)
def list_projects(self):
return self._client.me['projects']
def list_users(self, project_id):
return self._client.get("/projects/{}/memberships".format(project_id))
def import_project(self, project_id, options={"template": "scrum", "users_bindings": {}, "keep_external_reference": False}):
(project, project_data) = self._import_project_data(project_id, options)
self._import_epics_data(project_data, project, options)
self._import_user_stories_data(project_data, project, options)
Timeline.objects.filter(project=project).delete()
rebuild_timeline(None, None, project.id)
recalc_reference_counter(project)
def _import_project_data(self, project_id, options):
project_data = self._client.get(
"/projects/{}".format(project_id),
{
"fields": ",".join([
"point_scale",
"name",
"description",
"labels(name)",
])
}
)
project_data['iterations'] = self._client.get(
"/projects/{}/iterations".format(project_id),
{
"fields": ",".join([
"number",
"start",
"finish",
"stories",
])
}
)
project_data['epics'] = self._client.get(
"/projects/{}/epics".format(project_data['id']),
{
"fields": ",".join([
"name",
"label",
"description",
"comments(text,file_attachments,google_attachments,person,created_at)",
"follower_ids",
"created_at",
"updated_at",
"url",
])
}
)
project_template = ProjectTemplate.objects.get(slug=options['template'])
project_template.is_epics_activated = True
project_template.us_statuses = []
project_template.points = [{
"value": None,
"name": "?",
"order": 1,
}]
counter = 2
for points in project_data['point_scale'].split(","):
project_template.points.append({
"value": int(points),
"name": points,
"order": counter
})
counter += 1
project_template.us_statuses.append({
"name": "Unscheduled",
"slug": "unscheduled",
"is_closed": True,
"is_archived": False,
"color": "#999999",
"wip_limit": None,
"order": 1,
})
project_template.us_statuses.append({
"name": "Unstarted",
"slug": "unstarted",
"is_closed": False,
"is_archived": False,
"color": "#999999",
"wip_limit": None,
"order": 2,
})
project_template.us_statuses.append({
"name": "Planned",
"slug": "planned",
"is_closed": False,
"is_archived": False,
"color": "#999999",
"wip_limit": None,
"order": 3,
})
project_template.us_statuses.append({
"name": "Started",
"slug": "started",
"is_closed": False,
"is_archived": False,
"color": "#999999",
"wip_limit": None,
"order": 4,
})
project_template.us_statuses.append({
"name": "Finished",
"slug": "finished",
"is_closed": False,
"is_archived": False,
"color": "#999999",
"wip_limit": None,
"order": 5,
})
project_template.us_statuses.append({
"name": "Delivered",
"slug": "delivered",
"is_closed": True,
"is_archived": False,
"color": "#999999",
"wip_limit": None,
"order": 6,
})
project_template.us_statuses.append({
"name": "Rejected",
"slug": "rejected",
"is_closed": True,
"is_archived": True,
"color": "#999999",
"wip_limit": None,
"order": 7,
})
project_template.us_statuses.append({
"name": "Accepted",
"slug": "accepted",
"is_closed": True,
"is_archived": True,
"color": "#999999",
"wip_limit": None,
"order": 8,
})
project_template.default_options["us_status"] = "Unscheduled"
project_template.task_statuses = []
project_template.task_statuses.append({
"name": "Incomplete",
"slug": "incomplete",
"is_closed": False,
"color": "#ff8a84",
"order": 1,
})
project_template.task_statuses.append({
"name": "Complete",
"slug": "complete",
"is_closed": True,
"color": "#669900",
"order": 2,
})
project_template.default_options["task_status"] = "Incomplete"
main_permissions = project_template.roles[0]['permissions']
project_template.roles = [{
"name": "Main",
"slug": "main",
"computable": True,
"permissions": main_permissions,
"order": 70,
}]
tags_colors = []
for label in project_data['labels']:
name = label['name'].lower()
tags_colors.append([name, None])
project = Project.objects.create(
name=project_data['name'],
description=project_data.get('description', ''),
owner=self._user,
tags_colors=tags_colors,
creation_template=project_template
)
UserStoryCustomAttribute.objects.create(
name="Due date",
description="Due date",
type="date",
order=1,
project=project
)
UserStoryCustomAttribute.objects.create(
name="Type",
description="Story type",
type="text",
order=2,
project=project
)
for user in options.get('users_bindings', {}).values():
if user != self._user:
Membership.objects.get_or_create(
user=user,
project=project,
role=project.get_roles().get(slug="main"),
is_admin=False,
)
for iteration in project_data['iterations']:
milestone = Milestone.objects.create(
name="Sprint {}".format(iteration['number']),
slug="sprint-{}".format(iteration['number']),
owner=self._user,
project=project,
estimated_start=iteration['start'][:10],
estimated_finish=iteration['finish'][:10],
)
Milestone.objects.filter(id=milestone.id).update(
created_date=iteration['start'],
modified_date=iteration['start'],
)
return (project, project_data)
def _import_user_stories_data(self, project_data, project, options):
users_bindings = options.get('users_bindings', {})
epics = {e['label']['id']: e for e in project_data['epics']}
due_date_field = project.userstorycustomattributes.get(name="Due date")
story_type_field = project.userstorycustomattributes.get(name="Type")
story_milestone_binding = {}
for iteration in project_data['iterations']:
for story in iteration['stories']:
story_milestone_binding[story['id']] = Milestone.objects.get(
project=project,
slug="sprint-{}".format(iteration['number'])
)
counter = 0
offset = 0
while True:
stories = self._client.get("/projects/{}/stories".format(project_data['id']), {
"envelope": "true",
"limit": 300,
"offset": offset,
"fields": ",".join([
"name",
"description",
"estimate",
"story_type",
"current_state",
"deadline",
"requested_by_id",
"owner_ids",
"labels(id,name)",
"comments(text,file_attachments,google_attachments,person,created_at)",
"tasks(id,description,position,complete,created_at,updated_at)",
"follower_ids",
"created_at",
"updated_at",
"url",
])})
offset += 300
for story in stories['data']:
tags = []
for label in story['labels']:
tags.append(label['name'])
assigned_to = None
if len(story['owner_ids']) > 0:
assigned_to = users_bindings.get(story['owner_ids'][0], None)
owner = users_bindings.get(story['requested_by_id'], self._user)
external_reference = None
if options.get('keep_external_reference', False):
external_reference = ["pivotal", story['url']]
us = UserStory.objects.create(
project=project,
owner=owner,
assigned_to=assigned_to,
status=project.us_statuses.get(slug=story['current_state']),
kanban_order=counter,
sprint_order=counter,
backlog_order=counter,
subject=story['name'],
description=story.get('description', ''),
tags=tags,
external_reference=external_reference,
milestone=story_milestone_binding.get(story['id'], None)
)
points = Points.objects.get(project=project, value=story.get('estimate', None))
RolePoints.objects.filter(user_story=us, role__slug="main").update(points_id=points.id)
if len(story['owner_ids']) > 1:
watchers = list(set(story['owner_ids'][1:] + story['follower_ids']))
else:
watchers = story['follower_ids']
for watcher in watchers:
watcher_user = users_bindings.get(watcher, None)
if watcher_user:
us.add_watcher(watcher_user)
if story.get('deadline', None):
us.custom_attributes_values.attributes_values = {due_date_field.id: story['deadline']}
us.custom_attributes_values.save()
if story.get('story_type', None):
us.custom_attributes_values.attributes_values = {story_type_field.id: story['story_type']}
us.custom_attributes_values.save()
UserStory.objects.filter(id=us.id).update(
ref=story['id'],
modified_date=story['updated_at'],
created_date=story['created_at']
)
take_snapshot(us, comment="", user=None, delete=False)
for label in story['labels']:
if epics.get(label['id'], None):
RelatedUserStory.objects.create(
epic=Epic.objects.get(project=project, ref=epics.get(label['id'])['id']),
user_story=us,
order=us.backlog_order
)
self._import_tasks(project_data, us, story)
self._import_user_story_activity(project_data, us, story, options)
self._import_comments(project_data, us, story, options)
counter += 1
if len(stories['data']) < 300:
break
def _import_epics_data(self, project_data, project, options):
users_bindings = options.get('users_bindings', {})
counter = 0
for epic in project_data['epics']:
external_reference = None
if options.get('keep_external_reference', False):
external_reference = ["pivotal", epic['url']]
taiga_epic = Epic.objects.create(
project=project,
owner=self._user,
status=project.epic_statuses.get(slug="new"),
epics_order=counter,
subject=epic['name'],
description=epic.get('description', ''),
tags=[],
external_reference=external_reference
)
Epic.objects.filter(id=taiga_epic.id).update(
ref=epic['id'],
modified_date=epic['updated_at'],
created_date=epic['created_at']
)
for watcher in epic['follower_ids']:
watcher_user = users_bindings.get(watcher, None)
if watcher_user:
taiga_epic.add_watcher(watcher_user)
take_snapshot(taiga_epic, comment="", user=None, delete=False)
self._import_comments(project_data, taiga_epic, epic, options)
self._import_epic_activity(project_data, taiga_epic, epic, options)
counter += 1
def _import_tasks(self, project_data, us, story):
for task in story['tasks']:
taiga_task = Task.objects.create(
subject=task['description'],
status=us.project.task_statuses.get(slug="complete" if task['complete'] else "incomplete"),
project=us.project,
us_order=task['position'],
taskboard_order=task['position'],
user_story=us
)
Task.objects.filter(id=taiga_task.id).update(
ref=task['id'],
modified_date=task['updated_at'],
created_date=task['created_at']
)
take_snapshot(taiga_task, comment="", user=None, delete=False)
def _import_attachment(self, obj, attachment_id, attachment_name, created_at, person_id, options):
users_bindings = options.get('users_bindings', {})
data = self._client.get_attachment(attachment_id)
att = Attachment(
owner=users_bindings.get(person_id, self._user),
project=obj.project,
content_type=ContentType.objects.get_for_model(obj),
object_id=obj.id,
name=attachment_name,
size=len(data),
created_date=created_at,
is_deprecated=False,
)
att.attached_file.save(attachment_name, ContentFile(data), save=True)
def _import_comments(self, project_data, obj, story, options):
users_bindings = options.get('users_bindings', {})
for comment in story['comments']:
if 'text' in comment:
snapshot = take_snapshot(
obj,
comment=comment['text'],
user=users_bindings.get(comment['person']['id'], User(full_name=comment['person']['name'])),
delete=False
)
HistoryEntry.objects.filter(id=snapshot.id).update(created_at=comment['created_at'])
for attachment in comment['file_attachments']:
self._import_attachment(
obj,
attachment['id'],
attachment['filename'],
comment['created_at'],
comment['person']['id'],
options
)
def _import_user_story_activity(self, project_data, us, story, options):
offset = 0
while True:
activities = self._client.get(
"/projects/{}/stories/{}/activity".format(
project_data['id'],
story['id'],
),
{"envelope": "true", "limit": 300, "offset": offset}
)
offset += 300
for activity in activities['data']:
self._import_activity(us, activity, options)
if len(activities['data']) < 300:
break
def _import_epic_activity(self, project_data, taiga_epic, epic, options):
offset = 0
while True:
activities = self._client.get(
"/projects/{}/epics/{}/activity".format(
project_data['id'],
epic['id'],
),
{"envelope": "true", "limit": 300, "offset": offset}
)
offset += 300
for activity in activities['data']:
self._import_activity(taiga_epic, activity, options)
if len(activities['data']) < 300:
break
def _import_activity(self, obj, activity, options):
activity_data = self._transform_activity_data(obj, activity, options)
if activity_data is None:
return
change_old = activity_data['change_old']
change_new = activity_data['change_new']
hist_type = activity_data['hist_type']
comment = activity_data['comment']
user = activity_data['user']
key = make_key_from_model_object(activity_data['obj'])
typename = get_typename_for_model_class(type(activity_data['obj']))
diff = make_diff_from_dicts(change_old, change_new)
fdiff = FrozenDiff(key, diff, {})
entry = HistoryEntry.objects.create(
user=user,
project_id=obj.project.id,
key=key,
type=hist_type,
snapshot=None,
diff=fdiff.diff,
values=make_diff_values(typename, fdiff),
comment=comment,
comment_html=mdrender(obj.project, comment),
is_hidden=False,
is_snapshot=False,
)
HistoryEntry.objects.filter(id=entry.id).update(created_at=activity['occurred_at'])
return HistoryEntry.objects.get(id=entry.id)
def _transform_activity_data(self, obj, activity, options):
users_bindings = options.get('users_bindings', {})
due_date_field = obj.project.userstorycustomattributes.get(name="Due date")
story_type_field = obj.project.userstorycustomattributes.get(name="Type")
user = {"pk": None, "name": activity.get('performed_by', {}).get('name', None)}
taiga_user = users_bindings.get(activity.get('performed_by', {}).get('id', None), None)
if taiga_user:
user = {"pk": taiga_user.id, "name": taiga_user.get_full_name()}
result = {
"change_old": {},
"change_new": {},
"hist_type": HistoryType.change,
"comment": "",
"user": user,
"obj": obj
}
if activity['kind'] == "story_create_activity":
UserStory.objects.filter(id=obj.id, created_date__gt=activity['occurred_at']).update(
created_date=activity['occurred_at'],
owner=users_bindings.get(activity["performed_by"]["id"], self._user)
)
return None
elif activity['kind'] == "epic_create_activity":
Epic.objects.filter(id=obj.id, created_date__gt=activity['occurred_at']).update(
created_date=activity['occurred_at'],
owner=users_bindings.get(activity["performed_by"]["id"], self._user)
)
return None
elif activity['kind'] in ["story_update_activity", "epic_update_activity"]:
for change in activity['changes']:
if change['change_type'] != "update" or change['kind'] not in ["story", "epic"]:
continue
if 'description' in change['new_values']:
result['change_old']["description"] = str(change['original_values']['description'])
result['change_new']["description"] = str(change['new_values']['description'])
result['change_old']["description_html"] = mdrender(obj.project, str(change['original_values']['description']))
result['change_new']["description_html"] = mdrender(obj.project, str(change['new_values']['description']))
if 'estimate' in change['new_values']:
old_points = None
if change['original_values']['estimate']:
estimation = change['original_values']['estimate']
(old_points, _) = Points.objects.get_or_create(
project=obj.project,
value=estimation,
defaults={
"name": str(estimation),
"order": estimation,
}
)
old_points = old_points.id
new_points = None
if change['new_values']['estimate']:
estimation = change['new_values']['estimate']
(new_points, _) = Points.objects.get_or_create(
project=obj.project,
value=estimation,
defaults={
"name": str(estimation),
"order": estimation,
}
)
new_points = new_points.id
result['change_old']["points"] = {obj.project.roles.get(slug="main").id: old_points}
result['change_new']["points"] = {obj.project.roles.get(slug="main").id: new_points}
if 'name' in change['new_values']:
result['change_old']["subject"] = change['original_values']['name']
result['change_new']["subject"] = change['new_values']['name']
if 'labels' in change['new_values']:
result['change_old']["tags"] = [l.lower() for l in change['original_values']['labels']]
result['change_new']["tags"] = [l.lower() for l in change['new_values']['labels']]
if 'current_state' in change['new_values']:
result['change_old']["status"] = obj.project.us_statuses.get(slug=change['original_values']['current_state']).id
result['change_new']["status"] = obj.project.us_statuses.get(slug=change['new_values']['current_state']).id
if 'story_type' in change['new_values']:
if "custom_attributes" not in result['change_old']:
result['change_old']["custom_attributes"] = []
if "custom_attributes" not in result['change_new']:
result['change_new']["custom_attributes"] = []
result['change_old']["custom_attributes"].append({
"name": "Type",
"value": change['original_values']['story_type'],
"id": story_type_field.id
})
result['change_new']["custom_attributes"].append({
"name": "Type",
"value": change['new_values']['story_type'],
"id": story_type_field.id
})
if 'deadline' in change['new_values']:
if "custom_attributes" not in result['change_old']:
result['change_old']["custom_attributes"] = []
if "custom_attributes" not in result['change_new']:
result['change_new']["custom_attributes"] = []
result['change_old']["custom_attributes"].append({
"name": "Due date",
"value": change['original_values']['deadline'],
"id": due_date_field.id
})
result['change_new']["custom_attributes"].append({
"name": "Due date",
"value": change['new_values']['deadline'],
"id": due_date_field.id
})
# TODO: Process owners_ids
elif activity['kind'] == "task_create_activity":
return None
elif activity['kind'] == "task_update_activity":
for change in activity['changes']:
if change['change_type'] != "update" or change['kind'] != "task":
continue
try:
task = Task.objects.get(project=obj.project, ref=change['id'])
if 'description' in change['new_values']:
result['change_old']["subject"] = change['original_values']['description']
result['change_new']["subject"] = change['new_values']['description']
result['obj'] = task
if 'complete' in change['new_values']:
result['change_old']["status"] = obj.project.task_statuses.get(slug="complete" if change['original_values']['complete'] else "incomplete").id
result['change_new']["status"] = obj.project.task_statuses.get(slug="complete" if change['new_values']['complete'] else "incomplete").id
result['obj'] = task
except Task.DoesNotExist:
return None
elif activity['kind'] == "comment_create_activity":
return None
elif activity['kind'] == "comment_update_activity":
return None
elif activity['kind'] == "story_move_activity":
return None
return result

View File

@ -0,0 +1,56 @@
# -*- 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 .importer import PivotalImporter
logger = logging.getLogger('taiga.importers.pivotal')
@app.task(bind=True)
def import_project(self, user_id, token, project_id, options):
user = User.object.get(id=user_id)
importer = PivotalImporter(user, token)
try:
project = importer.import_project(project_id, options)
except Exception as e:
# Error
ctx = {
"user": user,
"error_subject": _("Error importing PivotalTracker project"),
"error_message": _("Error importing PivotalTracker project"),
"project": project_id,
"exception": e
}
email = mail_builder.pivotal_import_error(admin, ctx)
email.send()
logger.error('Error importing PivotalTracker project %s (by %s)', project_id, user, exc_info=sys.exc_info())
else:
ctx = {
"project": project,
"user": user,
}
email = mail_builder.pivotal_import_success(user, ctx)
email.send()

View File

@ -0,0 +1,16 @@
from taiga.users.models import User
def resolve_users_bindings(users_bindings):
new_users_bindings = {}
for key,value in users_bindings.items():
if isinstance(value, str):
try:
new_users_bindings[int(key)] = User.objects.get(email_iexact=value)
except User.MultipleObjectsReturned:
new_users_bindings[int(key)] = User.objects.get(email=value)
except User.DoesNotExists:
new_users_bindings[int(key)] = None
else:
new_users_bindings[int(key)] = User.objects.get(id=value)
return new_users_bindings

View File

@ -0,0 +1,148 @@
# -*- 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/>.
import uuid
from django.utils.translation import ugettext as _
from django.conf import settings
from taiga.base.api import viewsets
from taiga.base import response
from taiga.base import exceptions as exc
from taiga.base.decorators import list_route
from taiga.users.models import AuthData, User
from taiga.users.services import get_user_photo_url
from taiga.users.gravatar import get_user_gravatar_id
from taiga.projects.serializers import ProjectSerializer
from .importer import TrelloImporter
from taiga.importers import permissions
from taiga.importers.services import resolve_users_bindings
from . import tasks
class TrelloImporterViewSet(viewsets.ViewSet):
permission_classes = (permissions.ImporterPermission,)
@list_route(methods=["POST"])
def list_users(self, request, *args, **kwargs):
self.check_permissions(request, "list_users", None)
token = request.DATA.get('token', None)
project_id = request.DATA.get('project', None)
if not project_id:
raise exc.WrongArguments(_("The project param is needed"))
importer = TrelloImporter(request.user, token)
users = importer.list_users(project_id)
for user in users:
user['user'] = None
if not user['email']:
continue
try:
taiga_user = User.objects.get(email=user['email'])
except User.DoesNotExist:
continue
user['user'] = {
'id': taiga_user.id,
'full_name': taiga_user.get_full_name(),
'gravatar_id': get_user_gravatar_id(taiga_user),
'photo': get_user_photo_url(taiga_user),
}
return response.Ok(users)
@list_route(methods=["POST"])
def list_projects(self, request, *args, **kwargs):
self.check_permissions(request, "list_projects", None)
token = request.DATA.get('token', None)
importer = TrelloImporter(request.user, token)
projects = importer.list_projects()
return response.Ok(projects)
@list_route(methods=["POST"])
def import_project(self, request, *args, **kwargs):
self.check_permissions(request, "import_project", None)
token = request.DATA.get('token', None)
project_id = request.DATA.get('project', None)
if not project_id:
raise exc.WrongArguments(_("The project param is needed"))
options = {
"name": request.DATA.get('name', None),
"description": request.DATA.get('description', None),
"template": request.DATA.get('template', "kanban"),
"users_bindings": resolve_users_bindings(request.DATA.get("users_bindings", {})),
"keep_external_reference": request.DATA.get("keep_external_reference", False),
"is_private": request.DATA.get("is_private", False),
}
if settings.CELERY_ENABLED:
task = tasks.import_project.delay(request.user.id, token, project_id, options)
return response.Accepted({"task_id": task.id})
importer = TrelloImporter(request.user, token)
project = importer.import_project(project_id, options)
project_data = {
"slug": project.slug,
"my_permissions": ["view_us"],
"is_backlog_activated": project.is_backlog_activated,
"is_kanban_activated": project.is_kanban_activated,
}
return response.Ok(project_data)
@list_route(methods=["GET"])
def auth_url(self, request, *args, **kwargs):
self.check_permissions(request, "auth_url", None)
(oauth_token, oauth_secret, url) = TrelloImporter.get_auth_url()
(auth_data, created) = AuthData.objects.get_or_create(
user=request.user,
key="trello-oauth",
defaults={
"value": uuid.uuid4().hex,
"extra": {},
}
)
auth_data.extra = {
"oauth_token": oauth_token,
"oauth_secret": oauth_secret,
}
auth_data.save()
return response.Ok({"url": url})
@list_route(methods=["POST"])
def authorize(self, request, *args, **kwargs):
self.check_permissions(request, "authorize", None)
try:
oauth_data = request.user.auth_data.get(key="trello-oauth")
oauth_token = oauth_data.extra['oauth_token']
oauth_secret = oauth_data.extra['oauth_secret']
oauth_verifier = request.DATA.get('code')
oauth_data.delete()
trello_token = TrelloImporter.get_access_token(oauth_token, oauth_secret, oauth_verifier)['oauth_token']
except Exception as e:
raise exc.WrongArguments(_("Invalid or expired auth token"))
return response.Ok({
"token": trello_token
})

View File

@ -0,0 +1,544 @@
# -*- 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.utils.translation import ugettext as _
from requests_oauthlib import OAuth1Session, OAuth1
from django.conf import settings
from django.core.files.base import ContentFile
from django.contrib.contenttypes.models import ContentType
import requests
import webcolors
from django.template.defaultfilters import slugify
from taiga.base import exceptions as exc
from taiga.projects.services import projects as projects_service
from taiga.projects.models import Project, ProjectTemplate, Membership
from taiga.projects.userstories.models import UserStory
from taiga.projects.tasks.models import Task
from taiga.projects.attachments.models import Attachment
from taiga.projects.history.services import (make_diff_from_dicts,
make_diff_values,
make_key_from_model_object,
get_typename_for_model_class,
FrozenDiff)
from taiga.projects.history.models import HistoryEntry
from taiga.projects.history.choices import HistoryType
from taiga.projects.custom_attributes.models import UserStoryCustomAttribute
from taiga.mdrender.service import render as mdrender
from taiga.timeline.rebuilder import rebuild_timeline
from taiga.timeline.models import Timeline
from taiga.front.templatetags.functions import resolve as resolve_front_url
from taiga.base import exceptions
class TrelloClient:
def __init__(self, api_key, api_secret, token):
self.api_key = api_key
self.api_secret = api_secret
self.token = token
if self.token:
self.oauth = OAuth1(
client_key=self.api_key,
client_secret=self.api_secret,
resource_owner_key=self.token
)
else:
self.oauth = None
def get(self, uri_path, query_params=None):
headers = {'Accept': 'application/json'}
if query_params is None:
query_params = {}
if uri_path[0] == '/':
uri_path = uri_path[1:]
url = 'https://api.trello.com/1/%s' % uri_path
response = requests.get(url, params=query_params, headers=headers, auth=self.oauth)
if response.status_code == 400:
raise exc.WrongArguments(_("Invalid Request: %s at %s") % (response.text, url))
if response.status_code == 401:
raise exc.AuthenticationFailed(_("Unauthorized: %s at %s") % (response.text, url))
if response.status_code == 403:
raise exc.PermissionDenied(_("Unauthorized: %s at %s") % (response.text, url))
if response.status_code == 404:
raise exc.NotFound(_("Resource Unavailable: %s at %s") % (response.text, url))
if response.status_code != 200:
raise exc.WrongArguments(_("Resource Unavailable: %s at %s") % (response.text, url))
return response.json()
class TrelloImporter:
def __init__(self, user, token):
self._user = user
self._cached_orgs = {}
self._client = TrelloClient(
api_key=settings.IMPORTERS.get('trello', {}).get('api_key', None),
api_secret=settings.IMPORTERS.get('trello', {}).get('secret_key', None),
token=token,
)
def list_projects(self):
projects_data = self._client.get("/members/me/boards", {
"fields": "id,name,desc,prefs,idOrganization",
"organization": "true",
"organization_fields": "prefs",
})
projects = []
for project in projects_data:
is_private = False
if project['prefs']['permissionLevel'] == "private":
is_private = True
if project['prefs']['permissionLevel'] == "org":
if 'organization' not in project:
is_private = True
elif project['organization']['prefs']['permissionLevel'] == "private":
is_private = True
projects.append({
"id": project['id'],
"name": project['name'],
"description": project['desc'],
"is_private": is_private,
})
return projects
def list_users(self, project_id):
members = []
for member in self._client.get("/board/{}/members/all".format(project_id), {"fields": "id"}):
user = self._client.get("/member/{}".format(member['id']), {"fields": "id,fullName,email,avatarSource,avatarHash,gravatarHash"})
print(user)
if user['avatarSource'] == "gravatar":
avatar = 'https://www.gravatar.com/avatar/' + user['gravatarHash'] + '.jpg?s=50'
else:
avatar = 'https://trello-avatars.s3.amazonaws.com/' + user['avatarHash'] + '/50.png'
members.append({
"id": user['id'],
"full_name": user['fullName'],
"email": user['email'],
"avatar": avatar
})
return members
def import_project(self, project_id, options):
data = self._client.get(
"/board/{}".format(project_id),
{
"fields": "name,desc",
"cards": "all",
"card_fields": "closed,labels,idList,desc,due,name,pos,dateLastActivity,idChecklists,idMembers,url",
"card_attachments": "true",
"labels": "all",
"labels_limit": "1000",
"lists": "all",
"list_fields": "closed,name,pos",
"members": "none",
"checklists": "all",
"checklist_fields": "name",
"organization": "true",
"organization_fields": "logoHash",
}
)
project = self._import_project_data(data, options)
self._import_user_stories_data(data, project, options)
self._cleanup(project, options)
Timeline.objects.filter(project=project).delete()
rebuild_timeline(None, None, project.id)
return project
def _import_project_data(self, data, options):
board = data
labels = board['labels']
statuses = board['lists']
project_template = ProjectTemplate.objects.get(slug=options.get('template', "kanban"))
project_template.us_statuses = []
counter = 0
for us_status in statuses:
if counter == 0:
project_template.default_options["us_status"] = us_status['name']
counter += 1
if us_status['name'] not in [s['name'] for s in project_template.us_statuses]:
project_template.us_statuses.append({
"name": us_status['name'],
"slug": slugify(us_status['name']),
"is_closed": False,
"is_archived": True if us_status['closed'] else False,
"color": "#999999",
"wip_limit": None,
"order": us_status['pos'],
})
project_template.task_statuses = []
project_template.task_statuses.append({
"name": "Incomplete",
"slug": "incomplete",
"is_closed": False,
"color": "#ff8a84",
"order": 1,
})
project_template.task_statuses.append({
"name": "Complete",
"slug": "complete",
"is_closed": True,
"color": "#669900",
"order": 2,
})
project_template.default_options["task_status"] = "Incomplete"
project_template.roles.append({
"name": "Trello",
"slug": "trello",
"computable": False,
"permissions": project_template.roles[0]['permissions'],
"order": 70,
})
tags_colors = []
for label in labels:
name = label['name']
if not name:
name = label['color']
name = name.lower()
color = self._ensure_hex_color(label['color'])
tags_colors.append([name, color])
project = Project(
name=options.get('name', None) or board['name'],
description=options.get('description', None) or board['desc'],
owner=self._user,
tags_colors=tags_colors,
creation_template=project_template,
is_private=options.get('is_private', False),
)
(can_create, error_message) = projects_service.check_if_project_can_be_created_or_updated(project)
if not can_create:
raise exceptions.NotEnoughSlotsForProject(project.is_private, 1, error_message)
project.save()
if board.get('organization', None):
trello_avatar_template = "https://trello-logos.s3.amazonaws.com/{}/170.png"
project_logo_url = trello_avatar_template.format(board['organization']['logoHash'])
data = requests.get(project_logo_url)
project.logo.save("logo.png", ContentFile(data.content), save=True)
UserStoryCustomAttribute.objects.create(
name="Due",
description="Due date",
type="date",
order=1,
project=project
)
for user in options.get('users_bindings', {}).values():
Membership.objects.create(
user=user,
project=project,
role=project.get_roles().get(slug="trello"),
is_admin=False,
invited_by=self._user,
)
return project
def _import_user_stories_data(self, data, project, options):
users_bindings = options.get('users_bindings', {})
statuses = {s['id']: s for s in data['lists']}
cards = data['cards']
due_date_field = project.userstorycustomattributes.first()
for card in cards:
if card['closed'] and not options.get("import_closed_data", False):
continue
if statuses[card['idList']]['closed'] and not options.get("import_closed_data", False):
continue
tags = []
for tag in card['labels']:
name = tag['name']
if not name:
name = tag['color']
name = name.lower()
tags.append(name)
assigned_to = None
if len(card['idMembers']) > 0:
assigned_to = users_bindings.get(card['idMembers'][0], None)
external_reference = None
if options.get('keep_external_reference', False):
external_reference = ["trello", card['url']]
us = UserStory.objects.create(
project=project,
owner=self._user,
assigned_to=assigned_to,
status=project.us_statuses.get(name=statuses[card['idList']]['name']),
kanban_order=card['pos'],
sprint_order=card['pos'],
backlog_order=card['pos'],
subject=card['name'],
description=card['desc'],
tags=tags,
external_reference=external_reference
)
if len(card['idMembers']) > 1:
for watcher in card['idMembers'][1:]:
watcher_user = users_bindings.get(watcher, None)
if watcher_user:
us.add_watcher(watcher_user)
if card['due']:
us.custom_attributes_values.attributes_values = {due_date_field.id: card['due']}
us.custom_attributes_values.save()
UserStory.objects.filter(id=us.id).update(
modified_date=card['dateLastActivity'],
created_date=card['dateLastActivity']
)
self._import_attachments(us, card, options)
self._import_tasks(data, us, card)
self._import_actions(us, card, statuses, options)
def _import_tasks(self, data, us, card):
checklists_by_id = {c['id']: c for c in data['checklists']}
for checklist_id in card['idChecklists']:
for item in checklists_by_id.get(checklist_id, {}).get('checkItems', []):
Task.objects.create(
subject=item['name'],
status=us.project.task_statuses.get(slug=item['state']),
project=us.project,
user_story=us
)
def _import_attachments(self, us, card, options):
users_bindings = options.get('users_bindings', {})
for attachment in card['attachments']:
if attachment['bytes'] is None:
continue
data = requests.get(attachment['url'])
att = Attachment(
owner=users_bindings.get(attachment['idMember'], self._user),
project=us.project,
content_type=ContentType.objects.get_for_model(UserStory),
object_id=us.id,
name=attachment['name'],
size=attachment['bytes'],
created_date=attachment['date'],
is_deprecated=False,
)
att.attached_file.save(attachment['name'], ContentFile(data.content), save=True)
UserStory.objects.filter(id=us.id, created_date__gt=attachment['date']).update(
created_date=attachment['date']
)
def _import_actions(self, us, card, statuses, options):
included_actions = [
"addAttachmentToCard", "addMemberToCard", "commentCard",
"convertToCardFromCheckItem", "copyCommentCard", "createCard",
"deleteAttachmentFromCard", "deleteCard", "removeMemberFromCard",
"updateCard",
]
actions = self._client.get(
"/card/{}/actions".format(card['id']),
{
"filter": ",".join(included_actions),
"limit": "1000",
"memberCreator": "true",
"memberCreator_fields": "fullName",
}
)
while actions:
for action in actions:
self._import_action(us, action, statuses, options)
actions = self._client.get(
"/card/{}/actions".format(card['id']),
{
"filter": ",".join(included_actions),
"limit": "1000",
"since": "lastView",
"before": action['date'],
"memberCreator": "true",
"memberCreator_fields": "fullName",
}
)
def _import_action(self, us, action, statuses, options):
key = make_key_from_model_object(us)
typename = get_typename_for_model_class(UserStory)
action_data = self._transform_action_data(us, action, statuses, options)
if action_data is None:
return
change_old = action_data['change_old']
change_new = action_data['change_new']
hist_type = action_data['hist_type']
comment = action_data['comment']
user = action_data['user']
diff = make_diff_from_dicts(change_old, change_new)
fdiff = FrozenDiff(key, diff, {})
entry = HistoryEntry.objects.create(
user=user,
project_id=us.project.id,
key=key,
type=hist_type,
snapshot=None,
diff=fdiff.diff,
values=make_diff_values(typename, fdiff),
comment=comment,
comment_html=mdrender(us.project, comment),
is_hidden=False,
is_snapshot=False,
)
HistoryEntry.objects.filter(id=entry.id).update(created_at=action['date'])
return HistoryEntry.objects.get(id=entry.id)
def _transform_action_data(self, us, action, statuses, options):
users_bindings = options.get('users_bindings', {})
due_date_field = us.project.userstorycustomattributes.first()
ignored_actions = ["addAttachmentToCard", "addMemberToCard",
"deleteAttachmentFromCard", "deleteCard",
"removeMemberFromCard"]
if action['type'] in ignored_actions:
return None
user = {"pk": None, "name": action.get('memberCreator', {}).get('fullName', None)}
taiga_user = users_bindings.get(action.get('memberCreator', {}).get('id', None), None)
if taiga_user:
user = {"pk": taiga_user.id, "name": taiga_user.get_full_name()}
result = {
"change_old": {},
"change_new": {},
"hist_type": HistoryType.change,
"comment": "",
"user": user
}
if action['type'] == "commentCard":
result['comment'] = str(action['data']['text'])
elif action['type'] == "convertToCardFromCheckItem":
UserStory.objects.filter(id=us.id, created_date__gt=action['date']).update(
created_date=action['date'],
owner=users_bindings.get(action["idMemberCreator"], self._user)
)
result['hist_type'] = HistoryType.create
elif action['type'] == "copyCommentCard":
UserStory.objects.filter(id=us.id, created_date__gt=action['date']).update(
created_date=action['date'],
owner=users_bindings.get(action["idMemberCreator"], self._user)
)
result['hist_type'] = HistoryType.create
elif action['type'] == "createCard":
UserStory.objects.filter(id=us.id, created_date__gt=action['date']).update(
created_date=action['date'],
owner=users_bindings.get(action["idMemberCreator"], self._user)
)
result['hist_type'] = HistoryType.create
elif action['type'] == "updateCard":
if 'desc' in action['data']['old']:
result['change_old']["description"] = str(action['data']['old'].get('desc', ''))
result['change_new']["description"] = str(action['data']['card'].get('desc', ''))
result['change_old']["description_html"] = mdrender(us.project, str(action['data']['old'].get('desc', '')))
result['change_new']["description_html"] = mdrender(us.project, str(action['data']['card'].get('desc', '')))
if 'idList' in action['data']['old']:
old_status_name = statuses[action['data']['old']['idList']]['name']
result['change_old']["status"] = us.project.us_statuses.get(name=old_status_name).id
new_status_name = statuses[action['data']['card']['idList']]['name']
result['change_new']["status"] = us.project.us_statuses.get(name=new_status_name).id
if 'name' in action['data']['old']:
result['change_old']["subject"] = action['data']['old']['name']
result['change_new']["subject"] = action['data']['card']['name']
if 'due' in action['data']['old']:
result['change_old']["custom_attributes"] = [{
"name": "Due",
"value": action['data']['old']['due'],
"id": due_date_field.id
}]
result['change_new']["custom_attributes"] = [{
"name": "Due",
"value": action['data']['card']['due'],
"id": due_date_field.id
}]
if result['change_old'] == {}:
return None
return result
@classmethod
def get_auth_url(cls):
request_token_url = 'https://trello.com/1/OAuthGetRequestToken'
authorize_url = 'https://trello.com/1/OAuthAuthorizeToken'
return_url = resolve_front_url("new-project-import", "trello")
expiration = "1day"
scope = "read,write,account"
trello_key = settings.IMPORTERS.get('trello', {}).get('api_key', None)
trello_secret = settings.IMPORTERS.get('trello', {}).get('secret_key', None)
name = "Taiga"
session = OAuth1Session(client_key=trello_key, client_secret=trello_secret)
response = session.fetch_request_token(request_token_url)
oauth_token, oauth_token_secret = response.get('oauth_token'), response.get('oauth_token_secret')
return (
oauth_token,
oauth_token_secret,
"{authorize_url}?oauth_token={oauth_token}&scope={scope}&expiration={expiration}&name={name}&return_url={return_url}".format(
authorize_url=authorize_url,
oauth_token=oauth_token,
expiration=expiration,
scope=scope,
name=name,
return_url=return_url,
)
)
@classmethod
def get_access_token(cls, oauth_token, oauth_token_secret, oauth_verifier):
api_key = settings.IMPORTERS.get('trello', {}).get('api_key', None)
api_secret = settings.IMPORTERS.get('trello', {}).get('secret_key', None)
access_token_url = 'https://trello.com/1/OAuthGetAccessToken'
session = OAuth1Session(client_key=api_key, client_secret=api_secret,
resource_owner_key=oauth_token, resource_owner_secret=oauth_token_secret,
verifier=oauth_verifier)
access_token = session.fetch_access_token(access_token_url)
return access_token
def _ensure_hex_color(self, color):
if color is None:
return None
try:
return webcolors.name_to_hex(color)
except ValueError:
return color
def _cleanup(self, project, options):
if not options.get("import_closed_data", False):
project.us_statuses.filter(is_archived=True).delete()

View File

@ -0,0 +1,56 @@
# -*- 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 .importer import TrelloImporter
logger = logging.getLogger('taiga.importers.trello')
@app.task(bind=True)
def import_project(self, user_id, token, project_id, options):
user = User.object.get(id=user_id)
importer = TrelloImporter(user, token)
try:
project = importer.import_project(project_id, options)
except Exception as e:
# Error
ctx = {
"user": user,
"error_subject": _("Error importing Trello project"),
"error_message": _("Error importing Trello project"),
"project": project_id,
"exception": e
}
email = mail_builder.trello_import_error(admin, ctx)
email.send()
logger.error('Error importing Trello project %s (by %s)', project_id, user, exc_info=sys.exc_info())
else:
ctx = {
"project": project,
"user": user,
}
email = mail_builder.trello_import_success(user, ctx)
email.send()

View File

@ -7,7 +7,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: taiga-back\n" "Project-Id-Version: taiga-back\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-02-09 09:12+0100\n" "POT-Creation-Date: 2017-02-15 12:07+0100\n"
"PO-Revision-Date: 2015-03-25 20:09+0100\n" "PO-Revision-Date: 2015-03-25 20:09+0100\n"
"Last-Translator: Taiga Dev Team <support@taiga.io>\n" "Last-Translator: Taiga Dev Team <support@taiga.io>\n"
"Language-Team: Taiga Dev Team <support@taiga.io>\n" "Language-Team: Taiga Dev Team <support@taiga.io>\n"
@ -184,8 +184,8 @@ msgstr ""
#: taiga/projects/api.py:754 taiga/projects/epics/api.py:200 #: taiga/projects/api.py:754 taiga/projects/epics/api.py:200
#: taiga/projects/epics/api.py:284 taiga/projects/issues/api.py:224 #: taiga/projects/epics/api.py:284 taiga/projects/issues/api.py:224
#: taiga/projects/mixins/ordering.py:59 taiga/projects/tasks/api.py:247 #: taiga/projects/mixins/ordering.py:59 taiga/projects/tasks/api.py:247
#: taiga/projects/tasks/api.py:272 taiga/projects/userstories/api.py:322 #: taiga/projects/tasks/api.py:272 taiga/projects/userstories/api.py:323
#: taiga/projects/userstories/api.py:374 taiga/webhooks/api.py:71 #: taiga/projects/userstories/api.py:375 taiga/webhooks/api.py:71
msgid "Blocked element" msgid "Blocked element"
msgstr "" msgstr ""
@ -764,7 +764,7 @@ msgstr ""
#: taiga/external_apps/models.py:39 taiga/projects/attachments/models.py:62 #: taiga/external_apps/models.py:39 taiga/projects/attachments/models.py:62
#: taiga/projects/custom_attributes/models.py:37 #: taiga/projects/custom_attributes/models.py:37
#: taiga/projects/epics/models.py:55 #: taiga/projects/epics/models.py:56
#: taiga/projects/history/templatetags/functions.py:25 #: taiga/projects/history/templatetags/functions.py:25
#: taiga/projects/issues/models.py:60 taiga/projects/models.py:152 #: taiga/projects/issues/models.py:60 taiga/projects/models.py:152
#: taiga/projects/models.py:739 taiga/projects/tasks/models.py:62 #: taiga/projects/models.py:739 taiga/projects/tasks/models.py:62
@ -801,7 +801,7 @@ msgstr ""
#: taiga/feedback/models.py:31 taiga/projects/attachments/models.py:48 #: taiga/feedback/models.py:31 taiga/projects/attachments/models.py:48
#: taiga/projects/contact/models.py:34 #: taiga/projects/contact/models.py:34
#: taiga/projects/custom_attributes/models.py:46 #: taiga/projects/custom_attributes/models.py:46
#: taiga/projects/epics/models.py:48 taiga/projects/issues/models.py:52 #: taiga/projects/epics/models.py:49 taiga/projects/issues/models.py:52
#: taiga/projects/likes/models.py:33 taiga/projects/milestones/models.py:48 #: taiga/projects/likes/models.py:33 taiga/projects/milestones/models.py:48
#: taiga/projects/models.py:159 taiga/projects/models.py:743 #: taiga/projects/models.py:159 taiga/projects/models.py:743
#: taiga/projects/notifications/models.py:89 taiga/projects/tasks/models.py:48 #: taiga/projects/notifications/models.py:89 taiga/projects/tasks/models.py:48
@ -861,7 +861,7 @@ msgstr ""
#: taiga/hooks/api.py:63 taiga/projects/epics/api.py:154 #: taiga/hooks/api.py:63 taiga/projects/epics/api.py:154
#: taiga/projects/issues/api.py:139 taiga/projects/tasks/api.py:201 #: taiga/projects/issues/api.py:139 taiga/projects/tasks/api.py:201
#: taiga/projects/userstories/api.py:276 #: taiga/projects/userstories/api.py:277
msgid "The project doesn't exist" msgid "The project doesn't exist"
msgstr "" msgstr ""
@ -949,6 +949,85 @@ msgstr ""
msgid "The status doesn't exist" msgid "The status doesn't exist"
msgstr "" msgstr ""
#: taiga/importers/asana/api.py:44 taiga/importers/asana/api.py:86
#: taiga/importers/github/api.py:45 taiga/importers/github/api.py:75
#: taiga/importers/jira/api.py:56 taiga/importers/jira/api.py:102
#: taiga/importers/pivotal/api.py:44 taiga/importers/pivotal/api.py:81
#: taiga/importers/trello/api.py:46 taiga/importers/trello/api.py:83
msgid "The project param is needed"
msgstr ""
#: taiga/importers/asana/api.py:51 taiga/importers/asana/api.py:74
#: taiga/importers/asana/api.py:131
msgid "Invalid Asana API request"
msgstr ""
#: taiga/importers/asana/api.py:53 taiga/importers/asana/api.py:76
#: taiga/importers/asana/api.py:133
msgid "Failed to make the request to Asana API"
msgstr ""
#: taiga/importers/asana/api.py:126 taiga/importers/github/api.py:121
msgid "Code param needed"
msgstr ""
#: taiga/importers/asana/tasks.py:42 taiga/importers/asana/tasks.py:43
msgid "Error importing Asana project"
msgstr ""
#: taiga/importers/github/api.py:129
msgid "Invalid auth data"
msgstr ""
#: taiga/importers/github/api.py:131
msgid "Third party service failing"
msgstr ""
#: taiga/importers/github/tasks.py:42 taiga/importers/github/tasks.py:43
msgid "Error importing GitHub project"
msgstr ""
#: taiga/importers/jira/api.py:58 taiga/importers/jira/api.py:85
#: taiga/importers/jira/api.py:105 taiga/importers/jira/api.py:178
msgid "The url param is needed"
msgstr ""
#: taiga/importers/jira/api.py:154
msgid "Invalid project_type {}"
msgstr ""
#: taiga/importers/jira/api.py:224 taiga/importers/pivotal/api.py:139
#: taiga/importers/trello/api.py:143
msgid "Invalid or expired auth token"
msgstr ""
#: taiga/importers/jira/tasks.py:47 taiga/importers/jira/tasks.py:48
msgid "Error importing Jira project"
msgstr ""
#: taiga/importers/pivotal/tasks.py:42 taiga/importers/pivotal/tasks.py:43
msgid "Error importing PivotalTracker project"
msgstr ""
#: taiga/importers/trello/importer.py:77
#, python-format
msgid "Invalid Request: %s at %s"
msgstr ""
#: taiga/importers/trello/importer.py:79 taiga/importers/trello/importer.py:81
#, python-format
msgid "Unauthorized: %s at %s"
msgstr ""
#: taiga/importers/trello/importer.py:83 taiga/importers/trello/importer.py:85
#, python-format
msgid "Resource Unavailable: %s at %s"
msgstr ""
#: taiga/importers/trello/tasks.py:42 taiga/importers/trello/tasks.py:43
msgid "Error importing Trello project"
msgstr ""
#: taiga/permissions/choices.py:23 taiga/permissions/choices.py:34 #: taiga/permissions/choices.py:23 taiga/permissions/choices.py:34
msgid "View project" msgid "View project"
msgstr "" msgstr ""
@ -1134,7 +1213,7 @@ msgid "Fans"
msgstr "" msgstr ""
#: taiga/projects/admin.py:144 taiga/projects/attachments/models.py:39 #: taiga/projects/admin.py:144 taiga/projects/attachments/models.py:39
#: taiga/projects/epics/models.py:39 taiga/projects/issues/models.py:37 #: taiga/projects/epics/models.py:40 taiga/projects/issues/models.py:37
#: taiga/projects/milestones/models.py:42 taiga/projects/models.py:164 #: taiga/projects/milestones/models.py:42 taiga/projects/models.py:164
#: taiga/projects/notifications/models.py:62 taiga/projects/tasks/models.py:39 #: taiga/projects/notifications/models.py:62 taiga/projects/tasks/models.py:39
#: taiga/projects/userstories/models.py:69 taiga/projects/wiki/models.py:40 #: taiga/projects/userstories/models.py:69 taiga/projects/wiki/models.py:40
@ -1217,7 +1296,7 @@ msgstr ""
#: taiga/projects/attachments/models.py:41 taiga/projects/contact/models.py:29 #: taiga/projects/attachments/models.py:41 taiga/projects/contact/models.py:29
#: taiga/projects/custom_attributes/models.py:43 #: taiga/projects/custom_attributes/models.py:43
#: taiga/projects/epics/models.py:37 taiga/projects/issues/models.py:50 #: taiga/projects/epics/models.py:38 taiga/projects/issues/models.py:50
#: taiga/projects/milestones/models.py:44 taiga/projects/models.py:506 #: taiga/projects/milestones/models.py:44 taiga/projects/models.py:506
#: taiga/projects/models.py:528 taiga/projects/models.py:565 #: taiga/projects/models.py:528 taiga/projects/models.py:565
#: taiga/projects/models.py:593 taiga/projects/models.py:619 #: taiga/projects/models.py:593 taiga/projects/models.py:619
@ -1240,7 +1319,7 @@ msgstr ""
#: taiga/projects/attachments/models.py:51 #: taiga/projects/attachments/models.py:51
#: taiga/projects/custom_attributes/models.py:48 #: taiga/projects/custom_attributes/models.py:48
#: taiga/projects/epics/models.py:51 taiga/projects/issues/models.py:55 #: taiga/projects/epics/models.py:52 taiga/projects/issues/models.py:55
#: taiga/projects/milestones/models.py:51 taiga/projects/models.py:162 #: taiga/projects/milestones/models.py:51 taiga/projects/models.py:162
#: taiga/projects/models.py:746 taiga/projects/tasks/models.py:51 #: taiga/projects/models.py:746 taiga/projects/tasks/models.py:51
#: taiga/projects/userstories/models.py:90 taiga/projects/wiki/models.py:47 #: taiga/projects/userstories/models.py:90 taiga/projects/wiki/models.py:47
@ -1266,7 +1345,7 @@ msgstr ""
#: taiga/projects/attachments/models.py:63 #: taiga/projects/attachments/models.py:63
#: taiga/projects/custom_attributes/models.py:41 #: taiga/projects/custom_attributes/models.py:41
#: taiga/projects/epics/models.py:101 taiga/projects/milestones/models.py:57 #: taiga/projects/epics/models.py:104 taiga/projects/milestones/models.py:57
#: taiga/projects/models.py:522 taiga/projects/models.py:555 #: taiga/projects/models.py:522 taiga/projects/models.py:555
#: taiga/projects/models.py:589 taiga/projects/models.py:613 #: taiga/projects/models.py:589 taiga/projects/models.py:613
#: taiga/projects/models.py:645 taiga/projects/models.py:665 #: taiga/projects/models.py:645 taiga/projects/models.py:665
@ -1409,26 +1488,26 @@ msgstr ""
msgid "You don't have permissions to set this status to this epic." msgid "You don't have permissions to set this status to this epic."
msgstr "" msgstr ""
#: taiga/projects/epics/models.py:35 taiga/projects/issues/models.py:35 #: taiga/projects/epics/models.py:36 taiga/projects/issues/models.py:35
#: taiga/projects/tasks/models.py:37 taiga/projects/userstories/models.py:62 #: taiga/projects/tasks/models.py:37 taiga/projects/userstories/models.py:62
msgid "ref" msgid "ref"
msgstr "" msgstr ""
#: taiga/projects/epics/models.py:42 taiga/projects/issues/models.py:39 #: taiga/projects/epics/models.py:43 taiga/projects/issues/models.py:39
#: taiga/projects/tasks/models.py:41 taiga/projects/userstories/models.py:72 #: taiga/projects/tasks/models.py:41 taiga/projects/userstories/models.py:72
msgid "status" msgid "status"
msgstr "" msgstr ""
#: taiga/projects/epics/models.py:45 #: taiga/projects/epics/models.py:46
msgid "epics order" msgid "epics order"
msgstr "" msgstr ""
#: taiga/projects/epics/models.py:54 taiga/projects/issues/models.py:59 #: taiga/projects/epics/models.py:55 taiga/projects/issues/models.py:59
#: taiga/projects/tasks/models.py:55 taiga/projects/userstories/models.py:94 #: taiga/projects/tasks/models.py:55 taiga/projects/userstories/models.py:94
msgid "subject" msgid "subject"
msgstr "" msgstr ""
#: taiga/projects/epics/models.py:58 taiga/projects/models.py:526 #: taiga/projects/epics/models.py:59 taiga/projects/models.py:526
#: taiga/projects/models.py:561 taiga/projects/models.py:617 #: taiga/projects/models.py:561 taiga/projects/models.py:617
#: taiga/projects/models.py:647 taiga/projects/models.py:667 #: taiga/projects/models.py:647 taiga/projects/models.py:667
#: taiga/projects/models.py:691 taiga/projects/models.py:719 #: taiga/projects/models.py:691 taiga/projects/models.py:719
@ -1436,23 +1515,28 @@ msgstr ""
msgid "color" msgid "color"
msgstr "" msgstr ""
#: taiga/projects/epics/models.py:61 taiga/projects/issues/models.py:63 #: taiga/projects/epics/models.py:62 taiga/projects/issues/models.py:63
#: taiga/projects/tasks/models.py:65 taiga/projects/userstories/models.py:98 #: taiga/projects/tasks/models.py:65 taiga/projects/userstories/models.py:98
msgid "assigned to" msgid "assigned to"
msgstr "" msgstr ""
#: taiga/projects/epics/models.py:63 taiga/projects/userstories/models.py:100 #: taiga/projects/epics/models.py:64 taiga/projects/userstories/models.py:100
msgid "is client requirement" msgid "is client requirement"
msgstr "" msgstr ""
#: taiga/projects/epics/models.py:65 taiga/projects/userstories/models.py:102 #: taiga/projects/epics/models.py:66 taiga/projects/userstories/models.py:102
msgid "is team requirement" msgid "is team requirement"
msgstr "" msgstr ""
#: taiga/projects/epics/models.py:69 #: taiga/projects/epics/models.py:70
msgid "user stories" msgid "user stories"
msgstr "" msgstr ""
#: taiga/projects/epics/models.py:72 taiga/projects/issues/models.py:66
#: taiga/projects/tasks/models.py:70 taiga/projects/userstories/models.py:109
msgid "external reference"
msgstr ""
#: taiga/projects/epics/validators.py:37 #: taiga/projects/epics/validators.py:37
msgid "There's no epic with that id" msgid "There's no epic with that id"
msgstr "" msgstr ""
@ -1635,11 +1719,6 @@ msgstr ""
msgid "finished date" msgid "finished date"
msgstr "" msgstr ""
#: taiga/projects/issues/models.py:66 taiga/projects/tasks/models.py:70
#: taiga/projects/userstories/models.py:109
msgid "external reference"
msgstr ""
#: taiga/projects/likes/models.py:36 #: taiga/projects/likes/models.py:36
msgid "Like" msgid "Like"
msgstr "" msgstr ""
@ -3307,25 +3386,25 @@ msgstr ""
msgid "Stakeholder" msgid "Stakeholder"
msgstr "" msgstr ""
#: taiga/projects/userstories/api.py:127 #: taiga/projects/userstories/api.py:128
msgid "You don't have permissions to set this sprint to this user story." msgid "You don't have permissions to set this sprint to this user story."
msgstr "" msgstr ""
#: taiga/projects/userstories/api.py:131 #: taiga/projects/userstories/api.py:132
msgid "You don't have permissions to set this status to this user story." msgid "You don't have permissions to set this status to this user story."
msgstr "" msgstr ""
#: taiga/projects/userstories/api.py:221 #: taiga/projects/userstories/api.py:222
#, python-brace-format #, python-brace-format
msgid "Invalid role id '{role_id}'" msgid "Invalid role id '{role_id}'"
msgstr "" msgstr ""
#: taiga/projects/userstories/api.py:228 #: taiga/projects/userstories/api.py:229
#, python-brace-format #, python-brace-format
msgid "Invalid points id '{points_id}'" msgid "Invalid points id '{points_id}'"
msgstr "" msgstr ""
#: taiga/projects/userstories/api.py:243 #: taiga/projects/userstories/api.py:244
#, python-brace-format #, python-brace-format
msgid "Generating the user story #{ref} - {subject}" msgid "Generating the user story #{ref} - {subject}"
msgstr "" msgstr ""

View File

@ -0,0 +1,21 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.2 on 2016-11-08 11:19
from __future__ import unicode_literals
import django.contrib.postgres.fields
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('epics', '0004_auto_20160928_0540'),
]
operations = [
migrations.AddField(
model_name='epic',
name='external_reference',
field=django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), blank=True, default=None, null=True, size=None, verbose_name='external reference'),
),
]

View File

@ -18,6 +18,7 @@
from django.db import models from django.db import models
from django.contrib.contenttypes.fields import GenericRelation from django.contrib.contenttypes.fields import GenericRelation
from django.contrib.postgres.fields import ArrayField
from django.conf import settings from django.conf import settings
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.utils import timezone from django.utils import timezone
@ -67,6 +68,8 @@ class Epic(OCCModelMixin, WatchedModelMixin, BlockedMixin, TaggedMixin, models.M
user_stories = models.ManyToManyField("userstories.UserStory", related_name="epics", user_stories = models.ManyToManyField("userstories.UserStory", related_name="epics",
through='RelatedUserStory', through='RelatedUserStory',
verbose_name=_("user stories")) verbose_name=_("user stories"))
external_reference = ArrayField(models.TextField(null=False, blank=False),
null=True, blank=True, default=None, verbose_name=_("external reference"))
attachments = GenericRelation("attachments.Attachment") attachments = GenericRelation("attachments.Attachment")

View File

@ -68,6 +68,19 @@ def make_reference(instance, project, create=False):
return refval, refinstance return refval, refinstance
def recalc_reference_counter(project):
seqname = make_sequence_name(project)
max_ref_us = project.user_stories.all().aggregate(max=models.Max('ref'))
max_ref_task = project.tasks.all().aggregate(max=models.Max('ref'))
max_ref_issue = project.issues.all().aggregate(max=models.Max('ref'))
max_references = list(filter(lambda x: x is not None, [max_ref_us['max'], max_ref_task['max'], max_ref_issue['max']]))
max_value = 0
if len(max_references) > 0:
max_value = max(max_references)
seq.set_max(seqname, max_value)
def create_sequence(sender, instance, created, **kwargs): def create_sequence(sender, instance, created, **kwargs):
if not created: if not created:
return return

View File

@ -106,6 +106,7 @@ class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixi
include_attachments = "include_attachments" in self.request.QUERY_PARAMS include_attachments = "include_attachments" in self.request.QUERY_PARAMS
include_tasks = "include_tasks" in self.request.QUERY_PARAMS include_tasks = "include_tasks" in self.request.QUERY_PARAMS
epic_id = self.request.QUERY_PARAMS.get("epic", None) epic_id = self.request.QUERY_PARAMS.get("epic", None)
# We can be filtering by more than one epic so epic_id can consist # We can be filtering by more than one epic so epic_id can consist
# of different ids separete by comma. In that situation we will use # of different ids separete by comma. In that situation we will use

View File

@ -17,6 +17,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from taiga.base import routers from taiga.base import routers
from django.conf import settings
router = routers.DefaultRouter(trailing_slash=False) router = routers.DefaultRouter(trailing_slash=False)
@ -283,6 +284,23 @@ from taiga.external_apps.api import Application, ApplicationToken
router.register(r"applications", Application, base_name="applications") router.register(r"applications", Application, base_name="applications")
router.register(r"application-tokens", ApplicationToken, base_name="application-tokens") router.register(r"application-tokens", ApplicationToken, base_name="application-tokens")
# Third party importers
if settings.IMPORTERS.get('trello', {}).get('active', False):
from taiga.importers.trello.api import TrelloImporterViewSet
router.register(r"importers/trello", TrelloImporterViewSet, base_name="importers-trello")
if settings.IMPORTERS.get('jira', {}).get('active', False):
from taiga.importers.jira.api import JiraImporterViewSet
router.register(r"importers/jira", JiraImporterViewSet, base_name="importers-jira")
if settings.IMPORTERS.get('github', {}).get('active', False):
from taiga.importers.github.api import GithubImporterViewSet
router.register(r"importers/github", GithubImporterViewSet, base_name="importers-github")
if settings.IMPORTERS.get('asana', {}).get('active', False):
from taiga.importers.asana.api import AsanaImporterViewSet
router.register(r"importers/asana", AsanaImporterViewSet, base_name="importers-asana")
# Stats # Stats
# - see taiga.stats.routers and taiga.stats.apps # - see taiga.stats.routers and taiga.stats.apps

View File

@ -0,0 +1,248 @@
# -*- 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.importers import exceptions
from taiga.base.utils import json
from taiga.base import exceptions as exc
pytestmark = pytest.mark.django_db
def test_auth_url(client, settings):
user = f.UserFactory.create()
client.login(user)
settings.IMPORTERS['asana']['callback_url'] = "http://testserver/url"
settings.IMPORTERS['asana']['app_id'] = "test-id"
settings.IMPORTERS['asana']['app_secret'] = "test-secret"
url = reverse("importers-asana-auth-url")
with mock.patch('taiga.importers.asana.api.AsanaImporter') as AsanaImporterMock:
AsanaImporterMock.get_auth_url.return_value = "https://auth_url"
response = client.get(url, content_type="application/json")
assert AsanaImporterMock.get_auth_url.calledWith(
settings.IMPORTERS['asana']['app_id'],
settings.IMPORTERS['asana']['app_secret'],
settings.IMPORTERS['asana']['callback_url']
)
assert response.status_code == 200
assert 'url' in response.data
assert response.data['url'] == "https://auth_url"
def test_authorize(client, settings):
user = f.UserFactory.create()
client.login(user)
authorize_url = reverse("importers-asana-authorize")
with mock.patch('taiga.importers.asana.api.AsanaImporter') as AsanaImporterMock:
AsanaImporterMock.get_access_token.return_value = "token"
response = client.post(authorize_url, content_type="application/json", data=json.dumps({"code": "code"}))
assert AsanaImporterMock.get_access_token.calledWith(
settings.IMPORTERS['asana']['app_id'],
settings.IMPORTERS['asana']['app_secret'],
"code"
)
assert response.status_code == 200
assert 'token' in response.data
assert response.data['token'] == "token"
def test_authorize_without_code(client):
user = f.UserFactory.create()
client.login(user)
authorize_url = reverse("importers-asana-authorize")
response = client.post(authorize_url, content_type="application/json", data=json.dumps({}))
assert response.status_code == 400
assert 'token' not in response.data
assert '_error_message' in response.data
assert response.data['_error_message'] == "Code param needed"
def test_authorize_with_bad_verify(client, settings):
user = f.UserFactory.create()
client.login(user)
authorize_url = reverse("importers-asana-authorize")
with mock.patch('taiga.importers.asana.api.AsanaImporter') as AsanaImporterMock:
AsanaImporterMock.get_access_token.side_effect = exceptions.InvalidRequest()
response = client.post(authorize_url, content_type="application/json", data=json.dumps({"code": "bad"}))
assert AsanaImporterMock.get_access_token.calledWith(
settings.IMPORTERS['asana']['app_id'],
settings.IMPORTERS['asana']['app_secret'],
"bad"
)
assert response.status_code == 400
assert 'token' not in response.data
assert '_error_message' in response.data
assert response.data['_error_message'] == "Invalid Asana API request"
def test_import_asana_list_users(client):
user = f.UserFactory.create()
client.login(user)
url = reverse("importers-asana-list-users")
with mock.patch('taiga.importers.asana.api.AsanaImporter') as AsanaImporterMock:
instance = mock.Mock()
instance.list_users.return_value = [
{"id": 1, "username": "user1", "full_name": "user1", "detected_user": None},
{"id": 2, "username": "user2", "full_name": "user2", "detected_user": None}
]
AsanaImporterMock.return_value = instance
response = client.post(url, content_type="application/json", data=json.dumps({"token": "token", "project": 1}))
assert response.status_code == 200
assert response.data[0]["id"] == 1
assert response.data[1]["id"] == 2
def test_import_asana_list_users_without_project(client):
user = f.UserFactory.create()
client.login(user)
url = reverse("importers-asana-list-users")
with mock.patch('taiga.importers.asana.api.AsanaImporter') as AsanaImporterMock:
instance = mock.Mock()
instance.list_users.return_value = [
{"id": 1, "username": "user1", "full_name": "user1", "detected_user": None},
{"id": 2, "username": "user2", "full_name": "user2", "detected_user": None}
]
AsanaImporterMock.return_value = instance
response = client.post(url, content_type="application/json", data=json.dumps({"token": "token"}))
assert response.status_code == 400
def test_import_asana_list_users_with_problem_on_request(client):
user = f.UserFactory.create()
client.login(user)
url = reverse("importers-asana-list-users")
with mock.patch('taiga.importers.asana.importer.AsanaClient') as AsanaClientMock:
instance = mock.Mock()
instance.workspaces.find_all.side_effect = exceptions.InvalidRequest()
AsanaClientMock.oauth.return_value = instance
response = client.post(url, content_type="application/json", data=json.dumps({"token": "token", "project": 1}))
assert response.status_code == 400
def test_import_asana_list_projects(client):
user = f.UserFactory.create()
client.login(user)
url = reverse("importers-asana-list-projects")
with mock.patch('taiga.importers.asana.api.AsanaImporter') as AsanaImporterMock:
instance = mock.Mock()
instance.list_projects.return_value = ["project1", "project2"]
AsanaImporterMock.return_value = instance
response = client.post(url, content_type="application/json", data=json.dumps({"token": "token"}))
assert response.status_code == 200
assert response.data[0] == "project1"
assert response.data[1] == "project2"
def test_import_asana_list_projects_with_problem_on_request(client):
user = f.UserFactory.create()
client.login(user)
url = reverse("importers-asana-list-projects")
with mock.patch('taiga.importers.asana.importer.AsanaClient') as AsanaClientMock:
instance = mock.Mock()
instance.workspaces.find_all.side_effect = exc.WrongArguments("Invalid Request")
AsanaClientMock.oauth.return_value = instance
response = client.post(url, content_type="application/json", data=json.dumps({"token": "token"}))
assert response.status_code == 400
def test_import_asana_project_without_project_id(client, settings):
settings.CELERY_ENABLED = True
user = f.UserFactory.create()
client.login(user)
url = reverse("importers-asana-import-project")
with mock.patch('taiga.importers.asana.tasks.AsanaImporter') as AsanaImporterMock:
response = client.post(url, content_type="application/json", data=json.dumps({"token": "token"}))
assert response.status_code == 400
def test_import_asana_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-asana-import-project")
with mock.patch('taiga.importers.asana.tasks.AsanaImporter') as AsanaImporterMock:
instance = mock.Mock()
instance.import_project.return_value = project
AsanaImporterMock.return_value = instance
response = client.post(url, content_type="application/json", data=json.dumps({"token": "token", "project": 1}))
assert response.status_code == 202
assert "task_id" in response.data
def test_import_asana_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-asana-import-project")
with mock.patch('taiga.importers.asana.api.AsanaImporter') as AsanaImporterMock:
instance = mock.Mock()
instance.import_project.return_value = project
AsanaImporterMock.return_value = instance
response = client.post(url, content_type="application/json", data=json.dumps({"token": "token", "project": 1}))
assert response.status_code == 200
assert "slug" in response.data
assert response.data['slug'] == "imported-project"

View File

@ -0,0 +1,236 @@
# -*- 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.importers import exceptions
from taiga.base.utils import json
from taiga.base import exceptions as exc
pytestmark = pytest.mark.django_db
def test_auth_url(client):
user = f.UserFactory.create()
client.login(user)
url = reverse("importers-github-auth-url")+"?uri=http://localhost:9001/project/new?from=github"
response = client.get(url, content_type="application/json")
assert response.status_code == 200
assert 'url' in response.data
assert response.data['url'] == "https://github.com/login/oauth/authorize?client_id=&scope=user,repo&redirect_uri=http://localhost:9001/project/new?from=github"
def test_authorize(client, settings):
user = f.UserFactory.create()
client.login(user)
authorize_url = reverse("importers-github-authorize")
with mock.patch('taiga.importers.github.api.GithubImporter') as GithubImporterMock:
GithubImporterMock.get_access_token.return_value = "token"
response = client.post(authorize_url, content_type="application/json", data=json.dumps({"code": "code"}))
assert GithubImporterMock.get_access_token.calledWith(
settings.IMPORTERS['github']['client_id'],
settings.IMPORTERS['github']['client_secret'],
"code"
)
assert response.status_code == 200
assert 'token' in response.data
assert response.data['token'] == "token"
def test_authorize_without_code(client):
user = f.UserFactory.create()
client.login(user)
authorize_url = reverse("importers-github-authorize")
response = client.post(authorize_url, content_type="application/json", data=json.dumps({}))
assert response.status_code == 400
assert 'token' not in response.data
assert '_error_message' in response.data
assert response.data['_error_message'] == "Code param needed"
def test_authorize_with_bad_verify(client, settings):
user = f.UserFactory.create()
client.login(user)
authorize_url = reverse("importers-github-authorize")
with mock.patch('taiga.importers.github.api.GithubImporter') as GithubImporterMock:
GithubImporterMock.get_access_token.side_effect = exceptions.InvalidAuthResult()
response = client.post(authorize_url, content_type="application/json", data=json.dumps({"code": "bad"}))
assert GithubImporterMock.get_access_token.calledWith(
settings.IMPORTERS['github']['client_id'],
settings.IMPORTERS['github']['client_secret'],
"bad"
)
assert response.status_code == 400
assert 'token' not in response.data
assert '_error_message' in response.data
assert response.data['_error_message'] == "Invalid auth data"
def test_import_github_list_users(client):
user = f.UserFactory.create()
client.login(user)
url = reverse("importers-github-list-users")
with mock.patch('taiga.importers.github.api.GithubImporter') as GithubImporterMock:
instance = mock.Mock()
instance.list_users.return_value = [
{"id": 1, "username": "user1", "full_name": "user1", "detected_user": None},
{"id": 2, "username": "user2", "full_name": "user2", "detected_user": None}
]
GithubImporterMock.return_value = instance
response = client.post(url, content_type="application/json", data=json.dumps({"token": "token", "project": 1}))
assert response.status_code == 200
assert response.data[0]["id"] == 1
assert response.data[1]["id"] == 2
def test_import_github_list_users_without_project(client):
user = f.UserFactory.create()
client.login(user)
url = reverse("importers-github-list-users")
with mock.patch('taiga.importers.github.api.GithubImporter') as GithubImporterMock:
instance = mock.Mock()
instance.list_users.return_value = [
{"id": 1, "username": "user1", "full_name": "user1", "detected_user": None},
{"id": 2, "username": "user2", "full_name": "user2", "detected_user": None}
]
GithubImporterMock.return_value = instance
response = client.post(url, content_type="application/json", data=json.dumps({"token": "token"}))
assert response.status_code == 400
def test_import_github_list_users_with_problem_on_request(client):
user = f.UserFactory.create()
client.login(user)
url = reverse("importers-github-list-users")
with mock.patch('taiga.importers.github.importer.GithubClient') as GithubClientMock:
instance = mock.Mock()
instance.get.side_effect = exc.WrongArguments("Invalid Request")
GithubClientMock.return_value = instance
response = client.post(url, content_type="application/json", data=json.dumps({"token": "token", "project": 1}))
assert response.status_code == 400
def test_import_github_list_projects(client):
user = f.UserFactory.create()
client.login(user)
url = reverse("importers-github-list-projects")
with mock.patch('taiga.importers.github.api.GithubImporter') as GithubImporterMock:
instance = mock.Mock()
instance.list_projects.return_value = ["project1", "project2"]
GithubImporterMock.return_value = instance
response = client.post(url, content_type="application/json", data=json.dumps({"token": "token"}))
assert response.status_code == 200
assert response.data[0] == "project1"
assert response.data[1] == "project2"
def test_import_github_list_projects_with_problem_on_request(client):
user = f.UserFactory.create()
client.login(user)
url = reverse("importers-github-list-projects")
with mock.patch('taiga.importers.github.importer.GithubClient') as GithubClientMock:
instance = mock.Mock()
instance.get.side_effect = exc.WrongArguments("Invalid Request")
GithubClientMock.return_value = instance
response = client.post(url, content_type="application/json", data=json.dumps({"token": "token"}))
assert response.status_code == 400
def test_import_github_project_without_project_id(client, settings):
settings.CELERY_ENABLED = True
user = f.UserFactory.create()
client.login(user)
url = reverse("importers-github-import-project")
with mock.patch('taiga.importers.github.tasks.GithubImporter') as GithubImporterMock:
response = client.post(url, content_type="application/json", data=json.dumps({"token": "token"}))
assert response.status_code == 400
def test_import_github_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-github-import-project")
with mock.patch('taiga.importers.github.tasks.GithubImporter') as GithubImporterMock:
instance = mock.Mock()
instance.import_project.return_value = project
GithubImporterMock.return_value = instance
response = client.post(url, content_type="application/json", data=json.dumps({"token": "token", "project": 1}))
assert response.status_code == 202
assert "task_id" in response.data
def test_import_github_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-github-import-project")
with mock.patch('taiga.importers.github.api.GithubImporter') as GithubImporterMock:
instance = mock.Mock()
instance.import_project.return_value = project
GithubImporterMock.return_value = instance
response = client.post(url, content_type="application/json", data=json.dumps({"token": "token", "project": 1}))
assert response.status_code == 200
assert "slug" in response.data
assert response.data['slug'] == "imported-project"

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"

View File

@ -0,0 +1,244 @@
# -*- 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
pytestmark = pytest.mark.django_db
def test_auth_url(client):
user = f.UserFactory.create()
client.login(user)
url = reverse("importers-trello-auth-url")
with mock.patch('taiga.importers.trello.importer.OAuth1Session') as OAuth1SessionMock:
session = mock.Mock()
session.fetch_request_token.return_value = {"oauth_token": "token", "oauth_token_secret": "token"}
OAuth1SessionMock.return_value = session
response = client.get(url, content_type="application/json")
assert response.status_code == 200
assert 'url' in response.data
assert response.data['url'] == "https://trello.com/1/OAuthAuthorizeToken?oauth_token=token&scope=read,write,account&expiration=1day&name=Taiga&return_url=http://localhost:9001/project/new/import/trello"
def test_authorize(client):
user = f.UserFactory.create()
client.login(user)
url = reverse("importers-trello-auth-url")
authorize_url = reverse("importers-trello-authorize")
with mock.patch('taiga.importers.trello.importer.OAuth1Session') as OAuth1SessionMock:
session = mock.Mock()
session.fetch_request_token.return_value = {"oauth_token": "token", "oauth_token_secret": "token"}
session.fetch_access_token.return_value = {"oauth_token": "token", "oauth_token_secret": "token"}
OAuth1SessionMock.return_value = session
client.get(url, content_type="application/json")
response = client.post(authorize_url, content_type="application/json", data=json.dumps({"oauth_verifier": "token"}))
assert response.status_code == 200
assert 'token' in response.data
assert response.data['token'] == "token"
def test_authorize_without_token_and_secret(client):
user = f.UserFactory.create()
client.login(user)
authorize_url = reverse("importers-trello-authorize")
with mock.patch('taiga.importers.trello.importer.OAuth1Session') as OAuth1SessionMock:
session = mock.Mock()
session.fetch_access_token.return_value = {"oauth_token": "token", "oauth_token_secret": "token"}
OAuth1SessionMock.return_value = session
response = client.post(authorize_url, content_type="application/json", data=json.dumps({"oauth_verifier": "token"}))
assert response.status_code == 400
assert 'token' not in response.data
def test_authorize_with_bad_verify(client):
user = f.UserFactory.create()
client.login(user)
url = reverse("importers-trello-auth-url")
authorize_url = reverse("importers-trello-authorize")
with mock.patch('taiga.importers.trello.importer.OAuth1Session') as OAuth1SessionMock:
session = mock.Mock()
session.fetch_request_token.return_value = {"oauth_token": "token", "oauth_token_secret": "token"}
session.fetch_access_token.side_effect = Exception("Bad Token")
OAuth1SessionMock.return_value = session
client.get(url, content_type="application/json")
response = client.post(authorize_url, content_type="application/json", data=json.dumps({"oauth_verifier": "token"}))
assert response.status_code == 400
assert 'token' not in response.data
def test_import_trello_list_users(client, settings):
user = f.UserFactory.create()
client.login(user)
url = reverse("importers-trello-list-users")
with mock.patch('taiga.importers.trello.api.TrelloImporter') as TrelloImporterMock:
instance = mock.Mock()
instance.list_users.return_value = [
{"id": 1, "fullName": "user1", "email": None},
{"id": 2, "fullName": "user2", "email": None}
]
TrelloImporterMock.return_value = instance
response = client.post(url, content_type="application/json", data=json.dumps({"token": "token", "project": 1}))
assert response.status_code == 200
assert response.data[0]["id"] == 1
assert response.data[1]["id"] == 2
def test_import_trello_list_users_without_project(client, settings):
user = f.UserFactory.create()
client.login(user)
url = reverse("importers-trello-list-users")
with mock.patch('taiga.importers.trello.api.TrelloImporter') as TrelloImporterMock:
instance = mock.Mock()
instance.list_users.return_value = [
{"id": 1, "fullName": "user1", "email": None},
{"id": 2, "fullName": "user2", "email": None}
]
TrelloImporterMock.return_value = instance
response = client.post(url, content_type="application/json", data=json.dumps({"token": "token"}))
assert response.status_code == 400
def test_import_trello_list_users_with_problem_on_request(client, settings):
user = f.UserFactory.create()
client.login(user)
url = reverse("importers-trello-list-users")
with mock.patch('taiga.importers.trello.importer.TrelloClient') as TrelloClientMock:
instance = mock.Mock()
instance.get.side_effect = exc.WrongArguments("Invalid Request")
TrelloClientMock.return_value = instance
response = client.post(url, content_type="application/json", data=json.dumps({"token": "token", "project": 1}))
assert response.status_code == 400
def test_import_trello_list_projects(client, settings):
user = f.UserFactory.create()
client.login(user)
url = reverse("importers-trello-list-projects")
with mock.patch('taiga.importers.trello.api.TrelloImporter') as TrelloImporterMock:
instance = mock.Mock()
instance.list_projects.return_value = ["project1", "project2"]
TrelloImporterMock.return_value = instance
response = client.post(url, content_type="application/json", data=json.dumps({"token": "token"}))
assert response.status_code == 200
assert response.data[0] == "project1"
assert response.data[1] == "project2"
def test_import_trello_list_projects_with_problem_on_request(client, settings):
user = f.UserFactory.create()
client.login(user)
url = reverse("importers-trello-list-projects")
with mock.patch('taiga.importers.trello.importer.TrelloClient') as TrelloClientMock:
instance = mock.Mock()
instance.get.side_effect = exc.WrongArguments("Invalid Request")
TrelloClientMock.return_value = instance
response = client.post(url, content_type="application/json", data=json.dumps({"token": "token"}))
assert response.status_code == 400
def test_import_trello_project_without_project_id(client, settings):
settings.CELERY_ENABLED = True
user = f.UserFactory.create()
client.login(user)
url = reverse("importers-trello-import-project")
with mock.patch('taiga.importers.trello.tasks.TrelloImporter') as TrelloImporterMock:
response = client.post(url, content_type="application/json", data=json.dumps({"token": "token"}))
assert response.status_code == 400
def test_import_trello_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-trello-import-project")
with mock.patch('taiga.importers.trello.tasks.TrelloImporter') as TrelloImporterMock:
instance = mock.Mock()
instance.import_project.return_value = project
TrelloImporterMock.return_value = instance
response = client.post(url, content_type="application/json", data=json.dumps({"token": "token", "project": 1}))
assert response.status_code == 202
assert "task_id" in response.data
def test_import_trello_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-trello-import-project")
with mock.patch('taiga.importers.trello.api.TrelloImporter') as TrelloImporterMock:
instance = mock.Mock()
instance.import_project.return_value = project
TrelloImporterMock.return_value = instance
response = client.post(url, content_type="application/json", data=json.dumps({"token": "token", "project": 1}))
assert response.status_code == 200
assert "slug" in response.data
assert response.data['slug'] == "imported-project"