diff --git a/requirements.txt b/requirements.txt index 11efe393..8f633141 100644 --- a/requirements.txt +++ b/requirements.txt @@ -39,3 +39,4 @@ psd-tools==1.4 CairoSVG==2.0.1 cryptography==1.7.1 PyJWT==1.4.2 +asana==0.6.2 diff --git a/settings/common.py b/settings/common.py index 9ce0e415..a0156181 100644 --- a/settings/common.py +++ b/settings/common.py @@ -567,6 +567,10 @@ GITHUB_API_CLIENT_SECRET = "" TRELLO_API_KEY = "" TRELLO_SECRET_KEY = "" +ASANA_APP_CALLBACK_URL = "" +ASANA_APP_ID = "" +ASANA_APP_SECRET = "" + JIRA_CONSUMER_KEY = "" JIRA_CERT = "" JIRA_PUB_CERT = "" diff --git a/taiga/importers/asana/api.py b/taiga/importers/asana/api.py new file mode 100644 index 00000000..3982a958 --- /dev/null +++ b/taiga/importers/asana/api.py @@ -0,0 +1,135 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2014-2016 Taiga Agile LLC +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from django.utils.translation import ugettext as _ +from django.conf import settings + +from taiga.base.api import viewsets +from taiga.base import response +from taiga.base import exceptions as exc +from taiga.base.decorators import list_route +from taiga.users.models import AuthData, User +from taiga.users.services import get_user_photo_url +from taiga.users.gravatar import get_user_gravatar_id +from taiga.projects.serializers import ProjectSerializer + +from taiga.importers import permissions, 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}) diff --git a/taiga/importers/asana/importer.py b/taiga/importers/asana/importer.py new file mode 100644 index 00000000..6c3c675a --- /dev/null +++ b/taiga/importers/asana/importer.py @@ -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) diff --git a/taiga/importers/asana/tasks.py b/taiga/importers/asana/tasks.py new file mode 100644 index 00000000..5d36bed9 --- /dev/null +++ b/taiga/importers/asana/tasks.py @@ -0,0 +1,56 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino +# Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +import logging +import sys + +from django.utils.translation import ugettext as _ + +from taiga.base.mails import mail_builder +from taiga.users.models import User +from taiga.celery import app +from .importer import 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() diff --git a/taiga/importers/exceptions.py b/taiga/importers/exceptions.py index da430079..ebb8de86 100644 --- a/taiga/importers/exceptions.py +++ b/taiga/importers/exceptions.py @@ -1,3 +1,6 @@ +class InvalidRequest(Exception): + pass + class InvalidAuthResult(Exception): pass diff --git a/taiga/importers/management/commands/import_from_asana.py b/taiga/importers/management/commands/import_from_asana.py new file mode 100644 index 00000000..d7d7dd21 --- /dev/null +++ b/taiga/importers/management/commands/import_from_asana.py @@ -0,0 +1,101 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino +# Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from django.core.management.base import BaseCommand +from django.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) diff --git a/taiga/routers.py b/taiga/routers.py index 961b7d19..382ede82 100644 --- a/taiga/routers.py +++ b/taiga/routers.py @@ -287,10 +287,12 @@ router.register(r"application-tokens", ApplicationToken, base_name="application- from taiga.importers.trello.api import TrelloImporterViewSet from taiga.importers.jira.api import JiraImporterViewSet 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/jira", JiraImporterViewSet, base_name="importers-jira") router.register(r"importers/github", GithubImporterViewSet, base_name="importers-github") +router.register(r"importers/asana", AsanaImporterViewSet, base_name="importers-asana") # Stats diff --git a/tests/integration/test_importers_asana_api.py b/tests/integration/test_importers_asana_api.py new file mode 100644 index 00000000..bc158694 --- /dev/null +++ b/tests/integration/test_importers_asana_api.py @@ -0,0 +1,236 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino +# Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +import 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"