Add asana importer
parent
cca421b14d
commit
477d964770
|
@ -39,3 +39,4 @@ psd-tools==1.4
|
||||||
CairoSVG==2.0.1
|
CairoSVG==2.0.1
|
||||||
cryptography==1.7.1
|
cryptography==1.7.1
|
||||||
PyJWT==1.4.2
|
PyJWT==1.4.2
|
||||||
|
asana==0.6.2
|
||||||
|
|
|
@ -567,6 +567,10 @@ GITHUB_API_CLIENT_SECRET = ""
|
||||||
TRELLO_API_KEY = ""
|
TRELLO_API_KEY = ""
|
||||||
TRELLO_SECRET_KEY = ""
|
TRELLO_SECRET_KEY = ""
|
||||||
|
|
||||||
|
ASANA_APP_CALLBACK_URL = ""
|
||||||
|
ASANA_APP_ID = ""
|
||||||
|
ASANA_APP_SECRET = ""
|
||||||
|
|
||||||
JIRA_CONSUMER_KEY = ""
|
JIRA_CONSUMER_KEY = ""
|
||||||
JIRA_CERT = ""
|
JIRA_CERT = ""
|
||||||
JIRA_PUB_CERT = ""
|
JIRA_PUB_CERT = ""
|
||||||
|
|
|
@ -0,0 +1,135 @@
|
||||||
|
# -*- 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 .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": 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.ASANA_APP_ID, settings.ASANA_APP_SECRET, settings.ASANA_APP_CALLBACK_URL)
|
||||||
|
|
||||||
|
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.ASANA_APP_ID, settings.ASANA_APP_SECRET, settings.ASANA_APP_CALLBACK_URL)
|
||||||
|
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})
|
|
@ -0,0 +1,351 @@
|
||||||
|
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"]):
|
||||||
|
users.append({
|
||||||
|
"id": user["id"],
|
||||||
|
"full_name": user['name'],
|
||||||
|
"detected_user": self._get_user(user)
|
||||||
|
})
|
||||||
|
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)
|
|
@ -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()
|
|
@ -1,3 +1,6 @@
|
||||||
|
class InvalidRequest(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
class InvalidAuthResult(Exception):
|
class InvalidAuthResult(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,101 @@
|
||||||
|
# -*- 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.ASANA_APP_ID, settings.ASANA_APP_SECRET, settings.ASANA_APP_CALLBACK_URL)
|
||||||
|
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.ASANA_APP_ID, settings.ASANA_APP_SECRET, settings.ASANA_APP_CALLBACK_URL)
|
||||||
|
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)
|
|
@ -287,10 +287,12 @@ router.register(r"application-tokens", ApplicationToken, base_name="application-
|
||||||
from taiga.importers.trello.api import TrelloImporterViewSet
|
from taiga.importers.trello.api import TrelloImporterViewSet
|
||||||
from taiga.importers.jira.api import JiraImporterViewSet
|
from taiga.importers.jira.api import JiraImporterViewSet
|
||||||
from taiga.importers.github.api import GithubImporterViewSet
|
from taiga.importers.github.api import GithubImporterViewSet
|
||||||
|
from taiga.importers.asana.api import AsanaImporterViewSet
|
||||||
|
|
||||||
router.register(r"importers/trello", TrelloImporterViewSet, base_name="importers-trello")
|
router.register(r"importers/trello", TrelloImporterViewSet, base_name="importers-trello")
|
||||||
router.register(r"importers/jira", JiraImporterViewSet, base_name="importers-jira")
|
router.register(r"importers/jira", JiraImporterViewSet, base_name="importers-jira")
|
||||||
router.register(r"importers/github", GithubImporterViewSet, base_name="importers-github")
|
router.register(r"importers/github", GithubImporterViewSet, base_name="importers-github")
|
||||||
|
router.register(r"importers/asana", AsanaImporterViewSet, base_name="importers-asana")
|
||||||
|
|
||||||
|
|
||||||
# Stats
|
# Stats
|
||||||
|
|
|
@ -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, settings):
|
||||||
|
user = f.UserFactory.create()
|
||||||
|
client.login(user)
|
||||||
|
settings.ASANA_APP_CALLBACK_URL = "http://testserver/url"
|
||||||
|
settings.ASANA_APP_ID = "test-id"
|
||||||
|
settings.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.ASANA_APP_ID, settings.ASANA_APP_SECRET, settings.ASANA_APP_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.ASANA_APP_ID, settings.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.ASANA_APP_ID, settings.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"
|
Loading…
Reference in New Issue