From 876fcf88e4a1ac0c4a02f928e4a6f02fc2a27633 Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Tue, 15 Nov 2016 09:34:52 +0100 Subject: [PATCH 001/113] Add first brick of the connector_jira addon Add project binding Add export of projects Add Jira project tasks model Add external dependencies in manifest Add security accesses Add basic import of tasks Task import are triggered by Jira webhooks. A batch import must be added later to import the changes from missed hooks or for those not wanting to use the webhooks. Add a basic backend adapter Batch import of project tasks Remove markdown The fields are not in markdown, but in a wiki syntax (proper to JIRA yet be determined) Link users and import tasks assignees Import worklogs Use an adapter for the project export Import issue types Filter type of issues to synchronize And assign the worklog to the first parent we find that we synchronize. If the type of the worklog's issue is synchronized, the worklog is assigned on this issue. If the worklog's issue is a sub-task, and the parent task of the sub-task is of a synchronized type, it link it to it. If the task has an epic, and the epics are synchronized, it link it to it. In last resort, the worklog will be linked with the project but with no task. Configure the name of the Epic Link field from API Create webhooks in JIRA from Odoo Choose template on project creation Discard Epics from different projects JIRA allow to choose an epic for any project. Setting the Epic as the Odoo task would create inconsistencies between the project and the task. We discard such an Epic, likely the task will be empty. Delete worklogs deleted on JIRA (only with webhooks) Delete tasks deleted on JIRA (only with webhooks) Add JIRA issue type in tasks Add JIRA Issue Key Add core business project types Improve display of JIRA backend Use a dedicated button to activate the Epic Link. Link the Epic in a dedicated field Add JIRA Parent for subtasks Show JIRA key in name Create project from shared project Check JIRA project keys Remove the button to 'one-click-export' a project It can't be working because we have other fields to configure on the binding. Add help on issue type synchronizations Accept tasks without parent Do not use id builtin --- connector_jira/README.rst | 8 + connector_jira/__init__.py | 6 + connector_jira/__openerp__.py | 38 ++ connector_jira/backend.py | 11 + connector_jira/consumer.py | 41 ++ connector_jira/controllers/__init__.py | 2 + connector_jira/controllers/main.py | 105 +++ connector_jira/data/cron.xml | 43 ++ connector_jira/models/__init__.py | 9 + .../models/account_analytic_line/__init__.py | 3 + .../models/account_analytic_line/common.py | 48 ++ .../models/account_analytic_line/importer.py | 214 +++++++ .../models/jira_backend/__init__.py | 2 + connector_jira/models/jira_backend/common.py | 599 ++++++++++++++++++ .../models/jira_binding/__init__.py | 2 + connector_jira/models/jira_binding/common.py | 31 + .../models/jira_issue_type/__init__.py | 3 + .../models/jira_issue_type/common.py | 36 ++ .../models/jira_issue_type/importer.py | 57 ++ .../models/project_project/__init__.py | 3 + .../models/project_project/common.py | 223 +++++++ .../models/project_project/exporter.py | 74 +++ .../models/project_task/__init__.py | 3 + connector_jira/models/project_task/common.py | 140 ++++ .../models/project_task/importer.py | 173 +++++ connector_jira/models/res_users/__init__.py | 3 + connector_jira/models/res_users/common.py | 90 +++ connector_jira/models/res_users/importer.py | 25 + connector_jira/related_action.py | 18 + connector_jira/security/ir.model.access.csv | 16 + connector_jira/unit/__init__.py | 4 + connector_jira/unit/backend_adapter.py | 20 + connector_jira/unit/binder.py | 68 ++ connector_jira/unit/exporter.py | 356 +++++++++++ connector_jira/unit/importer.py | 450 +++++++++++++ connector_jira/unit/mapper.py | 160 +++++ connector_jira/views/jira_backend_views.xml | 209 ++++++ .../views/jira_issue_type_views.xml | 33 + connector_jira/views/jira_menus.xml | 10 + .../views/project_project_views.xml | 74 +++ connector_jira/views/project_task_views.xml | 106 ++++ connector_jira/views/res_users_views.xml | 47 ++ connector_jira/wizards/__init__.py | 2 + connector_jira/wizards/jira_backend_auth.py | 145 +++++ .../wizards/jira_backend_auth_views.xml | 63 ++ 45 files changed, 3773 insertions(+) create mode 100644 connector_jira/README.rst create mode 100644 connector_jira/__init__.py create mode 100644 connector_jira/__openerp__.py create mode 100644 connector_jira/backend.py create mode 100644 connector_jira/consumer.py create mode 100644 connector_jira/controllers/__init__.py create mode 100644 connector_jira/controllers/main.py create mode 100644 connector_jira/data/cron.xml create mode 100644 connector_jira/models/__init__.py create mode 100644 connector_jira/models/account_analytic_line/__init__.py create mode 100644 connector_jira/models/account_analytic_line/common.py create mode 100644 connector_jira/models/account_analytic_line/importer.py create mode 100644 connector_jira/models/jira_backend/__init__.py create mode 100644 connector_jira/models/jira_backend/common.py create mode 100644 connector_jira/models/jira_binding/__init__.py create mode 100644 connector_jira/models/jira_binding/common.py create mode 100644 connector_jira/models/jira_issue_type/__init__.py create mode 100644 connector_jira/models/jira_issue_type/common.py create mode 100644 connector_jira/models/jira_issue_type/importer.py create mode 100644 connector_jira/models/project_project/__init__.py create mode 100644 connector_jira/models/project_project/common.py create mode 100644 connector_jira/models/project_project/exporter.py create mode 100644 connector_jira/models/project_task/__init__.py create mode 100644 connector_jira/models/project_task/common.py create mode 100644 connector_jira/models/project_task/importer.py create mode 100644 connector_jira/models/res_users/__init__.py create mode 100644 connector_jira/models/res_users/common.py create mode 100644 connector_jira/models/res_users/importer.py create mode 100644 connector_jira/related_action.py create mode 100644 connector_jira/security/ir.model.access.csv create mode 100644 connector_jira/unit/__init__.py create mode 100644 connector_jira/unit/backend_adapter.py create mode 100644 connector_jira/unit/binder.py create mode 100644 connector_jira/unit/exporter.py create mode 100644 connector_jira/unit/importer.py create mode 100644 connector_jira/unit/mapper.py create mode 100644 connector_jira/views/jira_backend_views.xml create mode 100644 connector_jira/views/jira_issue_type_views.xml create mode 100644 connector_jira/views/jira_menus.xml create mode 100644 connector_jira/views/project_project_views.xml create mode 100644 connector_jira/views/project_task_views.xml create mode 100644 connector_jira/views/res_users_views.xml create mode 100644 connector_jira/wizards/__init__.py create mode 100644 connector_jira/wizards/jira_backend_auth.py create mode 100644 connector_jira/wizards/jira_backend_auth_views.xml diff --git a/connector_jira/README.rst b/connector_jira/README.rst new file mode 100644 index 000000000..4b4f47cee --- /dev/null +++ b/connector_jira/README.rst @@ -0,0 +1,8 @@ +JIRA Connector +============== + +Known Issues: + +* The tasks and worklogs deleted on JIRA are deleted if + the webhooks are active and running, but the batch + import can't see what has been deleted on Jira... diff --git a/connector_jira/__init__.py b/connector_jira/__init__.py new file mode 100644 index 000000000..0f2719533 --- /dev/null +++ b/connector_jira/__init__.py @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- +from . import backend +from . import controllers +from . import models +from . import unit +from . import wizards diff --git a/connector_jira/__openerp__.py b/connector_jira/__openerp__.py new file mode 100644 index 000000000..89328b903 --- /dev/null +++ b/connector_jira/__openerp__.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) + +{'name': 'Connector Jira', + 'version': '9.0.1.0.0', + 'author': 'Camptocamp,Odoo Community Association (OCA)', + 'license': 'AGPL-3', + 'category': 'Connector', + 'depends': ['connector', + 'project', + 'project_timesheet', + 'web', + ], + 'external_dependencies': { + 'python': [ + 'requests', + 'jira', + 'oauthlib', + # 'requests-oauthlib', + # 'requests-toolbelt', + # 'PyJWT', + 'cryptography', + ], + }, + 'website': 'http://www.camptocamp.com', + 'data': [ + 'views/jira_menus.xml', + 'wizards/jira_backend_auth_views.xml', + 'views/jira_backend_views.xml', + 'views/project_project_views.xml', + 'views/project_task_views.xml', + 'views/res_users_views.xml', + 'views/jira_issue_type_views.xml', + 'security/ir.model.access.csv', + 'data/cron.xml', + ], + 'installable': True, + } diff --git a/connector_jira/backend.py b/connector_jira/backend.py new file mode 100644 index 000000000..1badbd16e --- /dev/null +++ b/connector_jira/backend.py @@ -0,0 +1,11 @@ +# -*- coding: utf-8 -*- +# Copyright 2016 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) + +from openerp.addons.connector.backend import Backend + +jira = Backend('jira') +""" Generic QoQa Backend. """ + +jira_7_2_0 = Backend(parent=jira, version='7.2.0') +""" Backend for version 7.2.0 of Jira """ diff --git a/connector_jira/consumer.py b/connector_jira/consumer.py new file mode 100644 index 000000000..39e5446ab --- /dev/null +++ b/connector_jira/consumer.py @@ -0,0 +1,41 @@ +# -*- coding: utf-8 -*- +# Copyright 2016 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) + +from .unit.exporter import export_record + + +def delay_export(session, model_name, record_id, vals, **kwargs): + """ Delay a job which export a binding record. + + (A binding record being a ``jira.res.partner``, + ``jira.product.product``, ...) + + The additional kwargs are passed to ``delay()``, they can be: + ``priority``, ``eta``, ``max_retries``. + """ + if session.env.context.get('connector_no_export'): + return + fields = vals.keys() + export_record.delay(session, model_name, record_id, + fields=fields, **kwargs) + + +def delay_export_all_bindings(session, model_name, record_id, vals, + **kwargs): + """ Delay a job which export all the bindings of a record. + + In this case, it is called on records of normal models and will delay + the export for all the bindings. + + The additional kwargs are passed to ``delay()``, they can be: + ``priority``, ``eta``, ``max_retries``. + """ + if session.env.context.get('connector_no_export'): + return + model = session.env[model_name] + record = model.browse(record_id) + fields = vals.keys() + for binding in record.jira_bind_ids: + export_record.delay(session, binding._model._name, binding.id, + fields=fields, **kwargs) diff --git a/connector_jira/controllers/__init__.py b/connector_jira/controllers/__init__.py new file mode 100644 index 000000000..757b12a1f --- /dev/null +++ b/connector_jira/controllers/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +from . import main diff --git a/connector_jira/controllers/main.py b/connector_jira/controllers/main.py new file mode 100644 index 000000000..29ffc06a8 --- /dev/null +++ b/connector_jira/controllers/main.py @@ -0,0 +1,105 @@ +# -*- coding: utf-8 -*- +# Copyright 2016 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) + +""" + +Receive webhooks from Jira + +Webhooks to create in Jira: + +1. Odoo Issues + URL: http://odoo:8069/connector_jira/webhooks/issue/${issue.id} + Events: Issue{created, updated, deleted} + Exclude body: yes + +1. Odoo Worklogs + URL: http://odoo:8069/connector_jira/webhooks/worklog + Events: Issue{created, updated, deleted} + Exclude body: no + + +JIRA could well send all the data in the webhook request's body, +which would avoid Odoo to make another GET to get this data, but +JIRA webhooks are potentially insecure as we don't know if it really +comes from JIRA. So we don't use the data sent by the webhook and the job +gets the data by itself (with the nice side-effect that the job is retryable). + +""" + +import logging + +import openerp +from openerp import http +from openerp.http import request +from openerp.addons.web.controllers.main import ensure_db + +from openerp.addons.connector.session import ConnectorSession + +from ..models.account_analytic_line.importer import ( + import_worklog, + delete_worklog, +) +from ..unit.importer import import_record, delete_record + +_logger = logging.getLogger(__name__) + + +class JiraWebhookController(http.Controller): + + @http.route('/connector_jira/webhooks/issue', + type='json', auth='none', csrf=False) + def webhook_issue(self, issue_id=None, **kw): + ensure_db() + request.uid = openerp.SUPERUSER_ID + env = request.env + backend = env['jira.backend'].search( + [('use_webhooks', '=', True)], + limit=1 + ) + if not backend: + _logger.warning('Received a webhook from Jira but cannot find a ' + 'Jira backend with webhooks activated') + return + + action = request.jsonrequest['webhookEvent'] + + worklog = request.jsonrequest['issue'] + issue_id = worklog['id'] + + session = ConnectorSession.from_env(env) + if action == 'jira:issue_deleted': + delete_record.delay(session, 'jira.project.task', + backend.id, issue_id) + else: + import_record.delay(session, 'jira.project.task', + backend.id, issue_id) + + @http.route('/connector_jira/webhooks/worklog', + type='json', auth='none', csrf=False) + def webhook_worklog(self, **kw): + ensure_db() + request.uid = openerp.SUPERUSER_ID + env = request.env + backend = env['jira.backend'].search( + [('use_webhooks', '=', True)], + limit=1 + ) + if not backend: + _logger.warning('Received a webhook from Jira but cannot find a ' + 'Jira backend with webhooks activated') + return + + action = request.jsonrequest['webhookEvent'] + + worklog = request.jsonrequest['worklog'] + issue_id = worklog['issueId'] + worklog_id = worklog['id'] + + session = ConnectorSession.from_env(env) + if action == 'worklog_deleted': + delete_worklog.delay(session, 'jira.account.analytic.line', + backend.id, issue_id, worklog_id) + else: + import_worklog.delay(session, 'jira.account.analytic.line', + backend.id, issue_id, worklog_id) diff --git a/connector_jira/data/cron.xml b/connector_jira/data/cron.xml new file mode 100644 index 000000000..eadc78068 --- /dev/null +++ b/connector_jira/data/cron.xml @@ -0,0 +1,43 @@ + + + + + JIRA - Import Project Tasks + + + 10 + minutes + -1 + + + + + + + + JIRA - Import Users + + + 10 + minutes + -1 + + + + + + + + JIRA - Import Worklogs + + + 10 + minutes + -1 + + + + + + + diff --git a/connector_jira/models/__init__.py b/connector_jira/models/__init__.py new file mode 100644 index 000000000..8c3beb2ce --- /dev/null +++ b/connector_jira/models/__init__.py @@ -0,0 +1,9 @@ +# -*- coding: utf-8 -*- +from . import jira_binding # must be before the others + +from . import account_analytic_line +from . import jira_backend +from . import jira_issue_type +from . import project_project +from . import project_task +from . import res_users diff --git a/connector_jira/models/account_analytic_line/__init__.py b/connector_jira/models/account_analytic_line/__init__.py new file mode 100644 index 000000000..9d854de96 --- /dev/null +++ b/connector_jira/models/account_analytic_line/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- +from . import common +from . import importer diff --git a/connector_jira/models/account_analytic_line/common.py b/connector_jira/models/account_analytic_line/common.py new file mode 100644 index 000000000..dd75c3bbd --- /dev/null +++ b/connector_jira/models/account_analytic_line/common.py @@ -0,0 +1,48 @@ +# -*- coding: utf-8 -*- +# Copyright 2016 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) + +from openerp import fields, models + +from ...unit.backend_adapter import JiraAdapter +from ...backend import jira + + +class JiraAccountAnalyticLine(models.Model): + _name = 'jira.account.analytic.line' + _inherit = 'jira.binding' + _inherits = {'account.analytic.line': 'openerp_id'} + _description = 'Jira Worklog' + + openerp_id = fields.Many2one(comodel_name='account.analytic.line', + string='Timesheet Line', + required=True, + index=True, + ondelete='restrict') + # The REST API needs issue id + worklog id, so we keep it along + # in case we'll need it for an eventual export + jira_issue_id = fields.Char() + + +class AccountAnalyticLine(models.Model): + _inherit = 'account.analytic.line' + + jira_bind_ids = fields.One2many( + comodel_name='jira.account.analytic.line', + inverse_name='openerp_id', + copy=False, + string='Worklog Bindings', + context={'active_test': False}, + ) + + +@jira +class WorklogAdapter(JiraAdapter): + _model_name = 'jira.account.analytic.line' + + def read(self, issue_id, worklog_id): + return self.client.worklog(issue_id, worklog_id).raw + + def search(self, issue_id): + """ Search worklogs of an issue """ + return self.client.worklogs(issue_id) diff --git a/connector_jira/models/account_analytic_line/importer.py b/connector_jira/models/account_analytic_line/importer.py new file mode 100644 index 000000000..906c5b3af --- /dev/null +++ b/connector_jira/models/account_analytic_line/importer.py @@ -0,0 +1,214 @@ +# -*- coding: utf-8 -*- +# Copyright 2016 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) + +from openerp import _ +from openerp.addons.connector.exception import MappingError +from openerp.addons.connector.unit.mapper import ( + ImportMapper, + mapping, + only_create, +) +from openerp.addons.connector.queue.job import job +from ...unit.importer import ( + DelayedBatchImporter, + JiraImporter, + JiraDeleter, +) +from ...unit.backend_adapter import JiraAdapter +from ...unit.mapper import iso8601_local_date, whenempty +from ...backend import jira + + +@jira +class AnalyticLineMapper(ImportMapper): + _model_name = 'jira.account.analytic.line' + + direct = [ + (whenempty('comment', _('missing description')), 'name'), + (iso8601_local_date('started'), 'date'), + ] + + @only_create + @mapping + def default(self, record): + return {'is_timesheet': True} + + @mapping + def issue(self, record): + return {'jira_issue_id': record['issueId']} + + @mapping + def duration(self, record): + spent = float(record['timeSpentSeconds']) + # amount is in float in odoo... 2h30 = 2.5 + return {'unit_amount': spent / 60 / 60} + + @mapping + def author(self, record): + jira_author = record['author'] + jira_author_key = jira_author['key'] + binder = self.binder_for('jira.res.users') + user = binder.to_openerp(jira_author_key, unwrap=True) + if not user: + email = jira_author['emailAddress'] + raise MappingError( + _('No user found with login "%s" or email "%s".' + 'You must create a user or link it manually if the ' + 'login/email differs.') % (jira_author_key, email) + ) + return {'user_id': user.id} + + @mapping + def project_and_task(self, record): + task_binding = self.options.task_binding + + if not task_binding: + issue = self.options.linked_issue + assert issue + project_binder = self.binder_for('jira.project.project') + jira_project_id = issue['fields']['project']['id'] + project = project_binder.to_openerp(jira_project_id, unwrap=True) + # we can link to any task so we create the worklog + # on the project without any task + return {'account_id': project.analytic_account_id.id} + + analytic = task_binding.project_id.analytic_account_id + return {'task_id': task_binding.openerp_id.id, + 'account_id': analytic.id} + + @mapping + def backend_id(self, record): + return {'backend_id': self.backend_record.id} + + +@jira +class AnalyticLineBatchImporter(DelayedBatchImporter): + """ Import the Jira worklogs + + For every id in in the list, a delayed job is created. + Import from a date + """ + _model_name = 'jira.account.analytic.line' + + +@jira +class AnalyticLineImporter(JiraImporter): + _model_name = 'jira.account.analytic.line' + + def __init__(self, environment): + super(AnalyticLineImporter, self).__init__(environment) + self.external_issue_id = None + self.task_binding = None + + def _recurse_import_task(self): + """ Import and return the task of proper type for the worklog + + As we decide which type of issues are imported for a project, + a worklog could be linked to an issue that we don't import. + In that case, we climb the parents of the issue until we find + a issue of a type we synchronize. + + It ensures that the 'to-be-linked' issue is imported and return it. + + """ + issue_adapter = self.unit_for(JiraAdapter, model='jira.project.task') + project_binder = self.binder_for('jira.project.project') + issue_binder = self.binder_for('jira.project.task') + issue_type_binder = self.binder_for('jira.issue.type') + jira_issue_id = self.external_record['issueId'] + epic_field_name = self.backend_record.epic_link_field_name + current_project_id = self.external_issue['fields']['project']['id'] + while jira_issue_id: + issue = issue_adapter.read( + jira_issue_id, + fields=['issuetype', 'project', 'parent', epic_field_name], + ) + jira_project_id = issue['fields']['project']['id'] + jira_issue_type_id = issue['fields']['issuetype']['id'] + project_binding = project_binder.to_openerp(jira_project_id) + issue_type_binding = issue_type_binder.to_openerp( + jira_issue_type_id + ) + # JIRA allows to set an EPIC of a different project. + # If it happens, we discard it. + if (jira_project_id == current_project_id and + issue_type_binding.is_sync_for_project(project_binding)): + break + if issue['fields'].get('parent'): + # 'parent' is used on sub-tasks relating to their parent task + jira_issue_id = issue['fields']['parent']['id'] + elif issue['fields'].get(epic_field_name): + # the epic link is set on a jira custom field + epic_key = issue['fields'][epic_field_name] + epic = issue_adapter.read(epic_key, fields='id') + # we got the key of the epic issue, so we translate + # it to the ID with a call to the API + jira_issue_id = epic['id'] + else: + # no parent issue of a type we are synchronizing has been + # found, the worklog will be assigned to no task + jira_issue_id = None + + if jira_issue_id: + self._import_dependency(jira_issue_id, 'jira.project.task') + return issue_binder.to_openerp(jira_issue_id) + + def _create_data(self, map_record, **kwargs): + _super = super(AnalyticLineImporter, self) + return _super._create_data(map_record, + task_binding=self.task_binding, + linked_issue=self.external_issue) + + def _update_data(self, map_record, **kwargs): + _super = super(AnalyticLineImporter, self) + return _super._update_data(map_record, + task_binding=self.task_binding, + linked_issue=self.external_issue) + + def run(self, external_id, force=False, record=None, **kwargs): + assert 'issue_id' in kwargs + self.external_issue_id = kwargs.pop('issue_id') + return super(AnalyticLineImporter, self).run( + external_id, force=force, record=record, **kwargs + ) + + def _get_external_data(self): + """ Return the raw Jira data for ``self.external_id`` """ + issue_adapter = self.unit_for(JiraAdapter, model='jira.project.task') + self.external_issue = issue_adapter.read(self.external_issue_id) + return self.backend_adapter.read(self.external_issue_id, + self.external_id) + + def _import_dependencies(self): + """ Import the dependencies for the record""" + self.task_binding = self._recurse_import_task() + jira_assignee = self.external_record['author'] + jira_key = jira_assignee.get('key') + self._import_dependency(jira_key, + 'jira.res.users', + record=jira_assignee) + + +@jira +class AnalyticLineDeleter(JiraDeleter): + _model_name = 'jira.account.analytic.line' + + +@job(default_channel='root.connector_jira.normal') +def import_worklog(session, model_name, backend_id, issue_id, worklog_id, + force=False): + """ Import a worklog from Jira """ + backend = session.env['jira.backend'].browse(backend_id) + with backend.get_environment(model_name, session=session) as connector_env: + importer = connector_env.get_connector_unit(JiraImporter) + importer.run(worklog_id, issue_id=issue_id, force=force) + + +@job(default_channel='root.connector_jira.normal') +def delete_worklog(session, model_name, backend_id, issue_id, worklog_id): + """ Delete a local workflow which has been deleted on JIRA """ + backend = session.env['jira.backend'].browse(backend_id) + with backend.get_environment(model_name, session=session) as connector_env: + deleter = connector_env.get_connector_unit(JiraDeleter) + deleter.run(worklog_id) diff --git a/connector_jira/models/jira_backend/__init__.py b/connector_jira/models/jira_backend/__init__.py new file mode 100644 index 000000000..54ad006d5 --- /dev/null +++ b/connector_jira/models/jira_backend/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +from . import common diff --git a/connector_jira/models/jira_backend/common.py b/connector_jira/models/jira_backend/common.py new file mode 100644 index 000000000..3dee897c2 --- /dev/null +++ b/connector_jira/models/jira_backend/common.py @@ -0,0 +1,599 @@ +# -*- coding: utf-8 -*- +# Copyright: 2015 LasLabs, Inc. +# Copyright 2016 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) + +import logging +import json +import urlparse + +from contextlib import contextmanager, closing +from datetime import datetime, timedelta +from os import urandom + +import psycopg2 +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.primitives.asymmetric import rsa +from jira import JIRA, JIRAError +from jira.utils import json_loads + +import openerp +from openerp import models, fields, api, exceptions, _ + +from openerp.addons.connector.connector import ConnectorEnvironment +from openerp.addons.connector.session import ConnectorSession + +from ...unit.importer import import_batch +from ...unit.backend_adapter import JiraAdapter +from ..jira_issue_type.importer import import_batch_issue_type +from ...backend import jira + +_logger = logging.getLogger(__name__) + +IMPORT_DELTA = 70 # seconds + + +@contextmanager +def new_env(env): + with api.Environment.manage(): + registry = openerp.modules.registry.RegistryManager.get(env.cr.dbname) + with closing(registry.cursor()) as cr: + new_env = api.Environment(cr, env.uid, env.context) + try: + yield new_env + except: + cr.rollback() + raise + else: + cr.commit() + + +class JiraBackend(models.Model): + _name = 'jira.backend' + _description = 'Jira Backend' + _inherit = 'connector.backend' + _backend_type = 'jira' + + RSA_BITS = 4096 + RSA_PUBLIC_EXPONENT = 65537 + KEY_LEN = 255 # 255 == max Atlassian db col len + + def _default_company(self): + return self.env['res.company']._company_default_get('jira.backend') + + def _default_consumer_key(self): + ''' Generate a rnd consumer key of length self.KEY_LEN ''' + return urandom(self.KEY_LEN).encode('hex')[:self.KEY_LEN] + + version = fields.Selection( + selection='_select_versions', + string='Jira Version', + required=True, + ) + uri = fields.Char(string='Jira URI') + name = fields.Char() + company_id = fields.Many2one( + comodel_name='res.company', + string="Company", + required=True, + default=lambda self: self._default_company(), + ) + state = fields.Selection( + selection=[('authentify', 'Authentify'), + ('setup', 'Setup'), + ('running', 'Running'), + ], + default='authentify', + required=True, + readonly=True, + ) + private_key = fields.Text( + readonly=True, + groups="connector.group_connector_manager", + ) + public_key = fields.Text(readonly=True) + consumer_key = fields.Char( + default=lambda self: self._default_consumer_key(), + readonly=True, + groups="connector.group_connector_manager", + ) + + access_token = fields.Char( + readonly=True, + groups="connector.group_connector_manager", + ) + access_secret = fields.Char( + readonly=True, + groups="connector.group_connector_manager", + ) + + verify_ssl = fields.Boolean(default=True, string="Verify SSL?") + + project_template = fields.Selection( + selection='_selection_project_template', + string='Default Project Template', + default='Scrum software development', + required=True, + ) + project_template_shared = fields.Char( + string='Default Shared Template Key', + ) + + use_webhooks = fields.Boolean( + string='Use Webhooks', + readonly=True, + help="Webhooks need to be configured on the Jira instance. " + "When activated, synchronization from Jira is blazing fast. " + "It can be activated only on one Jira backend at a time. " + ) + + import_project_task_from_date = fields.Datetime( + compute='_compute_last_import_date', + inverse='_inverse_import_project_task_from_date', + string='Import Project Tasks from date', + ) + + import_analytic_line_from_date = fields.Datetime( + compute='_compute_last_import_date', + inverse='_inverse_import_analytic_line_from_date', + string='Import Worklogs from date', + ) + + issue_type_ids = fields.One2many( + comodel_name='jira.issue.type', + inverse_name='backend_id', + string='Issue Types', + readonly=True, + ) + + epic_link_field_name = fields.Char( + string='Epic Link Field', + help="The 'Epic Link' field on JIRA is a custom field. " + "The name of the field is something like 'customfield_10002'. " + ) + + odoo_webhook_base_url = fields.Char( + string='Base Odoo URL for Webhooks', + default=lambda self: self._default_odoo_webhook_base_url(), + ) + webhook_issue_jira_id = fields.Char() + webhook_worklog_jira_id = fields.Char() + + @api.model + def _default_odoo_webhook_base_url(self): + params = self.env['ir.config_parameter'] + return params.get_param('web.base.url', '') + + @api.model + def _selection_project_template(self): + return [('Scrum software development', + 'Scrum software development (Software)'), + ('Kanban software development', + 'Kanban software development (Software)'), + ('Basic software development', + 'Basic software development (Software)'), + ('Project management', 'Project management (Business)'), + ('Task management', 'Task management (Business)'), + ('Process management', 'Process management (Business)'), + ('shared', 'From a shared template'), + ] + + @api.model + def _select_versions(self): + """ Available versions + + Can be inherited to add custom versions. + """ + return [('7.2.0', '7.2.0+'), + ] + + @api.constrains('project_template_shared') + def check_jira_key(self): + for backend in self: + if not backend.project_template_shared: + continue + valid = self.env['jira.project.project']._jira_key_valid + if not valid(backend.project_template_shared): + raise exceptions.ValidationError( + _('%s is not a valid JIRA Key') % + backend.project_template_shared + ) + + @api.multi + @api.depends() + def _compute_last_import_date(self): + for backend in self: + self.env.cr.execute(""" + SELECT from_date_field, import_start_time + FROM jira_backend_timestamp + WHERE backend_id = %s""", (backend.id,)) + rows = self.env.cr.dictfetchall() + for row in rows: + field = row['from_date_field'] + timestamp = row['import_start_time'] + if field in self._fields: + backend[field] = timestamp + + @api.multi + def _inverse_date_fields(self, field_name): + for rec in self: + timestamp_id = self._lock_timestamp(field_name) + self._update_timestamp(timestamp_id, field_name, + getattr(rec, field_name)) + + @api.multi + def _inverse_import_project_task_from_date(self): + self._inverse_date_fields('import_project_task_from_date') + + @api.multi + def _inverse_import_analytic_line_from_date(self): + self._inverse_date_fields('import_analytic_line_from_date') + + + @api.multi + def _lock_timestamp(self, from_date_field): + """ Update the timestamp for a synchro + + thus, we prevent 2 synchros to be launched at the same time. + The lock is released at the commit of the transaction. + + Return the id of the timestamp if the lock could be acquired. + """ + assert from_date_field + self.ensure_one() + query = """ + SELECT id FROM jira_backend_timestamp + WHERE backend_id = %s + AND from_date_field = %s + FOR UPDATE NOWAIT + """ + try: + self.env.cr.execute( + query, (self.id, from_date_field) + ) + except psycopg2.OperationalError: + raise exceptions.UserError( + _("The synchronization timestamp %s is currently locked, " + "probably due to an ongoing synchronization." % + from_date_field) + ) + row = self.env.cr.fetchone() + return row[0] if row else None + + @api.multi + def _update_timestamp(self, timestamp_id, + from_date_field, import_start_time): + """ Update import timestamp for a synchro + + This method is called to update or create one import timestamp + for a jira.backend. A concurrency error can arise, but it's + handled in _import_from_date. + """ + self.ensure_one() + if not import_start_time: + return + if timestamp_id: + timestamp = self.env['jira.backend.timestamp'].browse(timestamp_id) + timestamp.import_start_time = import_start_time + else: + self.env['jira.backend.timestamp'].create({ + 'backend_id': self.id, + 'from_date_field': from_date_field, + 'import_start_time': import_start_time, + }) + + @api.multi + def _import_from_date(self, model, from_date_field): + """ Import records from a date + + Create jobs and update the sync timestamp in a savepoint; if a + concurrency issue arises, it will be logged and rollbacked silently. + """ + self.ensure_one() + with self.env.cr.savepoint(): + session = ConnectorSession.from_env(self.env) + import_start_time = datetime.now() + try: + self._lock_timestamp(from_date_field) + except exceptions.UserError: + # lock could not be acquired, it is already running and + # locked by another transaction + _logger.warning("Failed to update timestamps " + "for backend: %s and field: %s", + self, from_date_field, exc_info=True) + return + from_date = self[from_date_field] + if from_date: + from_date = fields.Datetime.from_string(from_date) + else: + from_date = None + import_batch.delay(session, model, self.id, + from_date=from_date, + to_date=import_start_time, + priority=9) + + # Reimport next records a small delta before the last import date + # in case of small lag between servers or transaction committed + # after the last import but with a date before the last import + # BTW, the JQL search of JIRA does not allow + # second precision, only minute precision, so + # we really have to take more than one minute + # margin + next_time = import_start_time - timedelta(seconds=IMPORT_DELTA) + next_time = fields.Datetime.to_string(next_time) + setattr(self, from_date_field, next_time) + + @api.constrains('use_webhooks') + def _check_use_webhooks_unique(self): + if len(self.search([('use_webhooks', '=', True)])) > 1: + raise exceptions.ValidationError( + _('Only one backend can listen to webhooks') + ) + + @api.model + def create(self, values): + record = super(JiraBackend, self).create(values) + record.create_rsa_key_vals() + return record + + @api.multi + def create_rsa_key_vals(self): + """ Create public/private RSA keypair """ + for backend in self: + private_key = rsa.generate_private_key( + public_exponent=self.RSA_PUBLIC_EXPONENT, + key_size=self.RSA_BITS, + backend=default_backend() + ) + pem = private_key.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.TraditionalOpenSSL, + encryption_algorithm=serialization.NoEncryption() + ) + public_pem = private_key.public_key().public_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PublicFormat.SubjectPublicKeyInfo + ) + backend.write({ + 'private_key': pem, + 'public_key': public_pem, + }) + + @api.multi + def button_setup(self): + self.state_running() + + @api.multi + def activate_epic_link(self): + self.ensure_one() + with self.get_environment('jira.backend') as connector_env: + adapter = connector_env.get_connector_unit(JiraAdapter) + jira_fields = adapter.list_fields() + for field in jira_fields: + custom_ref = field.get('schema', {}).get('custom') + if custom_ref == u'com.pyxis.greenhopper.jira:gh-epic-link': + self.epic_link_field_name = field['id'] + break + + @api.multi + def state_setup(self): + for backend in self: + if backend.state == 'authentify': + backend.state = 'setup' + + @api.multi + def state_running(self): + for backend in self: + if backend.state == 'setup': + backend.state = 'running' + + @api.multi + def create_webhooks(self): + self.ensure_one() + other_using_webhook = self.search( + [('use_webhooks', '=', True), + ('id', '!=', self.id)] + ) + if other_using_webhook: + raise exceptions.UserError( + _('Only one JIRA backend can use the webhook at a time. ' + 'You must disable them on the backend "%s" before ' + 'activating them here.') % (other_using_webhook.name,) + ) + + # open a new cursor because we'll commit after the creations + # to be sure to keep the webhook ids + with new_env(self.env) as env: + backend = env[self._name].browse(self.id) + base_url = backend.odoo_webhook_base_url + if not base_url: + raise exceptions.UserError( + _('The Odoo Webhook base URL must be set.') + ) + with backend.get_environment(self._name) as connector_env: + backend.use_webhooks = True + + adapter = connector_env.get_connector_unit(JiraAdapter) + # TODO: we could update the JQL of the webhook + # each time a new project is sync'ed, so we would + # filter out the useless events + url = urlparse.urljoin(base_url, + '/connector_jira/webhooks/issue') + webhook = adapter.create_webhook( + name='Odoo Issues', + url=url, + events=['jira:issue_created', + 'jira:issue_updated', + 'jira:issue_deleted', + ], + ) + # the only place where to find the hook id is in + # the 'self' url, looks like + # u'http://jira:8080/rest/webhooks/1.0/webhook/5' + webhook_id = webhook['self'].split('/')[-1] + backend.webhook_issue_jira_id = webhook_id + env.cr.commit() + + url = urlparse.urljoin(base_url, + '/connector_jira/webhooks/worklog') + webhook = adapter.create_webhook( + name='Odoo Worklogs', + url=url, + events=['worklog_created', + 'worklog_updated', + 'worklog_deleted', + ], + ) + webhook_id = webhook['self'].split('/')[-1] + backend.webhook_worklog_jira_id = webhook_id + env.cr.commit() + + @api.onchange('odoo_webhook_base_url') + def onchange_odoo_webhook_base_url(self): + if self.use_webhooks: + msg = _('If you change the base URL, you must delete and create ' + 'the Webhooks again.') + return {'warning': {'title': _('Warning'), 'message': msg}} + + @api.multi + def delete_webhooks(self): + self.ensure_one() + with self.get_environment('jira.backend') as connector_env: + adapter = connector_env.get_connector_unit(JiraAdapter) + if self.webhook_issue_jira_id: + try: + adapter.delete_webhook(self.webhook_issue_jira_id) + except JIRAError as err: + # 404 means it has been deleted in JIRA, ignore it + if err.status_code != 404: + raise + if self.webhook_worklog_jira_id: + try: + adapter.delete_webhook(self.webhook_worklog_jira_id) + except JIRAError as err: + # 404 means it has been deleted in JIRA, ignore it + if err.status_code != 404: + raise + self.use_webhooks = False + + @api.multi + def check_connection(self): + self.ensure_one() + try: + self.get_api_client() + except ValueError as err: + raise exceptions.UserError( + _('Failed to connect (%s)') % (err,) + ) + except JIRAError as err: + raise exceptions.UserError( + _('Failed to connect (%s)') % (err.text,) + ) + raise exceptions.UserError( + _('Connection successful') + ) + + @api.multi + def import_project_task(self): + self._import_from_date('jira.project.task', + 'import_project_task_from_date') + return True + + @api.multi + def import_analytic_line(self): + self._import_from_date('jira.account.analytic.line', + 'import_analytic_line_from_date') + return True + + @api.multi + def import_res_users(self): + self.env['res.users'].search([]).link_with_jira(backends=self) + return True + + @api.multi + def import_issue_type(self): + session = ConnectorSession.from_env(self.env) + import_batch_issue_type(session, 'jira.issue.type', self.id) + return True + + @contextmanager + @api.multi + def get_environment(self, model_name, session=None): + self.ensure_one() + if not session: + session = ConnectorSession.from_env(self.env) + yield ConnectorEnvironment(self, session, model_name) + + @api.model + def get_api_client(self): + oauth = { + 'access_token': self.access_token, + 'access_token_secret': self.access_secret, + 'consumer_key': self.consumer_key, + 'key_cert': self.private_key, + } + options = { + 'server': self.uri, + 'verify': self.verify_ssl, + } + return JIRA(options=options, oauth=oauth) + + @api.model + def _scheduler_import_project_task(self): + self.search([]).import_project_task() + + @api.model + def _scheduler_import_res_users(self): + self.search([]).import_res_users() + + @api.model + def _scheduler_import_analytic_line(self): + self.search([]).import_analytic_line() + + +class JiraBackendTimestamp(models.Model): + _name = 'jira.backend.timestamp' + _description = 'Jira Backend Import Timestamps' + + backend_id = fields.Many2one( + comodel_name='jira.backend', + string='Jira Backend', + required=True, + ) + from_date_field = fields.Char( + string='From Date Field', + required=True, + ) + import_start_time = fields.Datetime( + string='Import Start Time', + required=True, + ) + + +@jira +class BackendAdapter(JiraAdapter): + _model_name = 'jira.backend' + + webhook_base_path = '{server}/rest/webhooks/1.0/{path}' + + def list_fields(self): + return self.client._get_json('field') + + def create_webhook(self, name=None, url=None, events=None, + jql='', exclude_body=False): + assert name and url and events + data = {'name': name, + 'url': url, + 'events': events, + 'jqlFilter': jql, + 'excludeIssueDetails': exclude_body, + } + url = self.client._get_url('webhook', base=self.webhook_base_path) + response = self.client._session.post(url, data=json.dumps(data)) + return json_loads(response) + + def delete_webhook(self, id_): + url = self.client._get_url('webhook/%s' % id_, + base=self.webhook_base_path) + return json_loads(self.client._session.delete(url)) diff --git a/connector_jira/models/jira_binding/__init__.py b/connector_jira/models/jira_binding/__init__.py new file mode 100644 index 000000000..54ad006d5 --- /dev/null +++ b/connector_jira/models/jira_binding/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +from . import common diff --git a/connector_jira/models/jira_binding/common.py b/connector_jira/models/jira_binding/common.py new file mode 100644 index 000000000..10fedf6ce --- /dev/null +++ b/connector_jira/models/jira_binding/common.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +# Copyright 2016 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) + +from openerp import fields, models + + +class JiraBinding(models.AbstractModel): + """ Abstract Model for the Bindings. + + All the models used as bindings between Jira and Odoo + (``jira.product.product``, ...) should ``_inherit`` it. + """ + _name = 'jira.binding' + _inherit = 'external.binding' + _description = 'Jira Binding (abstract)' + + # openerp-side id must be declared in concrete model + # openerp_id = fields.Many2one(...) + backend_id = fields.Many2one( + comodel_name='jira.backend', + string='Jira Backend', + required=True, + ondelete='restrict', + ) + external_id = fields.Char(string='ID on Jira', index=True) + + _sql_constraints = [ + ('jira_binding_uniq', 'unique(backend_id, external_id)', + "A binding already exists for this Jira record"), + ] diff --git a/connector_jira/models/jira_issue_type/__init__.py b/connector_jira/models/jira_issue_type/__init__.py new file mode 100644 index 000000000..9d854de96 --- /dev/null +++ b/connector_jira/models/jira_issue_type/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- +from . import common +from . import importer diff --git a/connector_jira/models/jira_issue_type/common.py b/connector_jira/models/jira_issue_type/common.py new file mode 100644 index 000000000..3ae9a5e87 --- /dev/null +++ b/connector_jira/models/jira_issue_type/common.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- +# Copyright 2016 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) + +from openerp import api, fields, models + +from ...unit.backend_adapter import JiraAdapter +from ...backend import jira + + +class JiraIssueType(models.Model): + _name = 'jira.issue.type' + _inherit = 'jira.binding' + _description = 'Jira Issue Type' + + name = fields.Char(required=True, readonly=True) + description = fields.Char(readonly=True) + + @api.multi + def is_sync_for_project(self, project_binding): + self.ensure_one() + if not project_binding: + return False + return self in project_binding.sync_issue_type_ids + + +@jira +class IssueTypeAdapter(JiraAdapter): + _model_name = 'jira.issue.type' + + def read(self, id_): + return self.client.issue_type(id_).raw + + def search(self): + issues = self.client.issue_types() + return [issue.id for issue in issues] diff --git a/connector_jira/models/jira_issue_type/importer.py b/connector_jira/models/jira_issue_type/importer.py new file mode 100644 index 000000000..1317656cb --- /dev/null +++ b/connector_jira/models/jira_issue_type/importer.py @@ -0,0 +1,57 @@ +# -*- coding: utf-8 -*- +# Copyright 2016 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) + + +from openerp.addons.connector.queue.job import job +from openerp.addons.connector.unit.mapper import ImportMapper, mapping +from ...unit.importer import ( + BatchImporter, + DirectBatchImporter, + JiraImporter, +) +from ...backend import jira + + +@jira +class IssueTypeMapper(ImportMapper): + _model_name = 'jira.issue.type' + + direct = [ + ('name', 'name'), + ('description', 'description'), + ] + + @mapping + def backend_id(self, record): + return {'backend_id': self.backend_record.id} + + +@jira +class IssueTypeBatchImporter(DirectBatchImporter): + """ Import the Jira Issue Types + + For every id in in the list of issue types, a delayed job is created. + Import from a date + """ + _model_name = 'jira.issue.type' + + def run(self): + """ Run the synchronization """ + record_ids = self.backend_adapter.search() + for record_id in record_ids: + self._import_record(record_id) + + +@jira +class IssueTypeImporter(JiraImporter): + _model_name = 'jira.issue.type' + + +@job(default_channel='root.connector_jira.import') +def import_batch_issue_type(session, model_name, backend_id): + """ Prepare a batch import of issue types from Jira """ + backend = session.env['jira.backend'].browse(backend_id) + with backend.get_environment(model_name, session=session) as connector_env: + importer = connector_env.get_connector_unit(BatchImporter) + importer.run() diff --git a/connector_jira/models/project_project/__init__.py b/connector_jira/models/project_project/__init__.py new file mode 100644 index 000000000..cb97d3d7b --- /dev/null +++ b/connector_jira/models/project_project/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- +from . import common +from . import exporter diff --git a/connector_jira/models/project_project/common.py b/connector_jira/models/project_project/common.py new file mode 100644 index 000000000..71220d727 --- /dev/null +++ b/connector_jira/models/project_project/common.py @@ -0,0 +1,223 @@ +# -*- coding: utf-8 -*- +# Copyright 2016 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) + +import json +import logging +import re +import tempfile + +from jira import JIRAError +from jira.utils import json_loads + +from openerp import api, fields, models, exceptions, _ + +from ...unit.backend_adapter import JiraAdapter +from ...backend import jira + +_logger = logging.getLogger(__name__) + + +class JiraProjectProject(models.Model): + _name = 'jira.project.project' + _inherit = 'jira.binding' + _inherits = {'project.project': 'openerp_id'} + _description = 'Jira Projects' + + openerp_id = fields.Many2one(comodel_name='project.project', + string='Project', + required=True, + index=True, + ondelete='restrict') + sync_issue_type_ids = fields.Many2many( + comodel_name='jira.issue.type', + string='Issue Levels to Synchronize', + required=True, + domain="[('backend_id', '=', backend_id)]", + help="Only issues of these levels are imported. " + "When a worklog is imported no a level which is " + "not sync'ed, it is attached to the nearest " + "sync'ed parent level. If no parent can be found, " + "it is attached to a special 'Unassigned' task.", + ) + project_template = fields.Selection( + selection='_selection_project_template', + string='Default Project Template', + default='Scrum software development', + required=True, + ) + project_template_shared = fields.Char( + string='Default Shared Template', + ) + + @api.model + def _selection_project_template(self): + return self.env['jira.backend']._selection_project_template() + + @api.onchange('backend_id') + def onchange_project_backend_id(self): + self.project_template = self.backend_id.project_template + self.project_template_shared = self.backend_id.project_template_shared + + @staticmethod + def _jira_key_valid(key): + return bool(re.match(r'^[A-Z][A-Z0-9]{1,9}$', key)) + + @api.constrains('project_template_shared') + def check_project_template_shared(self): + for binding in self: + if not binding.project_template_shared: + continue + if not self._jira_key_valid(binding.project_template_shared): + raise exceptions.ValidationError( + _('%s is not a valid JIRA Key') % + binding.project_template_shared + ) + + @api.model + def create(self, values): + record = super(JiraProjectProject, self).create(values) + if not record.jira_key: + raise exceptions.UserError( + _('The JIRA Key is mandatory in order to export a project') + ) + return record + + @api.multi + def write(self, values): + if 'project_template' in values: + raise exceptions.UserError( + _('The project template cannot be modified.') + ) + return super(JiraProjectProject, self).write(values) + + @api.multi + def unlink(self): + if any(self.mapped('external_id')): + raise exceptions.UserError( + _('Exported project cannot be deleted.') + ) + return super(JiraProjectProject, self).unlink() + + +class ProjectProject(models.Model): + _inherit = 'project.project' + + jira_bind_ids = fields.One2many( + comodel_name='jira.project.project', + inverse_name='openerp_id', + copy=False, + string='Project Bindings', + context={'active_test': False}, + ) + jira_exportable = fields.Boolean( + string='Exportable on Jira', + compute='_compute_jira_exportable', + ) + jira_key = fields.Char( + string='JIRA Key', + size=10, # limit on JIRA + ) + + @api.constrains('jira_key') + def check_jira_key(self): + for project in self: + if not project.jira_key: + continue + valid = self.env['jira.project.project']._jira_key_valid + if not valid(project.jira_key): + raise exceptions.ValidationError( + _('%s is not a valid JIRA Key') % project.jira_key + ) + + @api.depends('jira_bind_ids') + def _compute_jira_exportable(self): + for project in self: + project.jira_exportable = bool(project.jira_bind_ids) + + @api.multi + def write(self, values): + result = super(ProjectProject, self).write(values) + for record in self: + if record.jira_exportable and not record.jira_key: + raise exceptions.UserError( + _('The JIRA Key is mandatory on JIRA projects.') + ) + return result + + @api.multi + def name_get(self): + names = [] + for project in self: + project_id, name = super(ProjectProject, project).name_get()[0] + if project.jira_key: + name = '[%s] %s' % (project.jira_key, name) + names.append((project_id, name)) + return names + + +@jira +class ProjectAdapter(JiraAdapter): + _model_name = 'jira.project.project' + + def read(self, id_): + return self.get(id_).raw + + def get(self, id_): + return self.client.project(id_) + + def write(self, id_, values): + self.get(id_).update(values) + + def create(self, key=None, name=None, template_name=None, values=None): + project = self.client.create_project( + key=key, + name=name, + template_name=template_name, + ) + if values: + project.update(values) + return project + + def create_shared(self, key=None, name=None, shared_key=None, lead=None): + assert key and name and shared_key + # There is no public method for creating a shared project: + # https://jira.atlassian.com/browse/JRA-45929 + # People found a private method for doing so, which is explained on: + # https://jira.atlassian.com/browse/JRA-27256?src=confmacro&_ga=1.162710906.750569280.1479368101 + + try: + project = self.read(shared_key) + project_id = project['id'] + except JIRAError as err: + if err.status_code == 404: + raise exceptions.UserError( + _('Project template with key "%s" not found.') % shared_key + ) + else: + raise + + url = (self.client._options['server'] + + '/rest/project-templates/1.0/createshared/%s' % project_id) + payload = {'name': name, + 'key': key, + 'lead': lead, + } + + r = self.client._session.post(url, data=json.dumps(payload)) + if r.status_code == 200: + r_json = json_loads(r) + return r_json + + f = tempfile.NamedTemporaryFile( + suffix='.html', + prefix='python-jira-error-create-shared-project-', + delete=False) + f.write(r.text) + + if self.logging: + logging.error( + "Unexpected result while running create shared project." + "Server response saved in %s for further investigation " + "[HTTP response=%s]." % (f.name, r.status_code)) + return False diff --git a/connector_jira/models/project_project/exporter.py b/connector_jira/models/project_project/exporter.py new file mode 100644 index 000000000..c878a96fb --- /dev/null +++ b/connector_jira/models/project_project/exporter.py @@ -0,0 +1,74 @@ +# -*- coding: utf-8 -*- +# Copyright 2016 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) + +from openerp.addons.connector.event import (on_record_create, + on_record_write, + ) +from ... import consumer +from ...backend import jira +from ...unit.exporter import JiraBaseExporter +from ...unit.backend_adapter import JiraAdapter + + +@on_record_create(model_names='jira.project.project') +@on_record_write(model_names='jira.project.project') +def delay_export(session, model_name, record_id, vals): + consumer.delay_export(session, model_name, record_id, vals, priority=10) + + +@on_record_write(model_names='project.project') +def delay_export_all_bindings(session, model_name, record_id, vals): + if vals.keys() == ['jira_bind_ids']: + # Binding edited from the project's view. + # When only this field has been modified, an other job has + # been delayed for the jira.product.product record. + return + consumer.delay_export_all_bindings(session, model_name, record_id, vals) + + +@jira +class JiraProjectProjectExporter(JiraBaseExporter): + _model_name = ['jira.project.project'] + + def _create_project(self, adapter, key, name, template, values): + project = adapter.create( + key=key, + name=name, + template_name=template, + values=values, + ) + return project['projectId'] + + def _create_shared_project(self, adapter, key, name, shared_key, lead): + project = adapter.create_shared( + key=key, + name=name, + shared_key=shared_key, + lead=lead, + ) + return project['projectId'] + + def _update_project(self, adapter, values): + adapter.write(self.external_id, values) + + def _run(self, fields=None): + adapter = self.unit_for(JiraAdapter) + + key = self.binding_record.jira_key + name = self.binding_record.name[:80] + template = self.binding_record.project_template + # TODO: add lead + + if self.external_id: + self._update_project(adapter, {'name': name, 'key': key}) + else: + if template == 'shared': + shared_key = self.binding_record.project_template_shared + self.external_id = self._create_shared_project( + adapter, key, name, shared_key, None + ) + else: + self.external_id = self._create_project( + adapter, key, name, template, {} + ) diff --git a/connector_jira/models/project_task/__init__.py b/connector_jira/models/project_task/__init__.py new file mode 100644 index 000000000..9d854de96 --- /dev/null +++ b/connector_jira/models/project_task/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- +from . import common +from . import importer diff --git a/connector_jira/models/project_task/common.py b/connector_jira/models/project_task/common.py new file mode 100644 index 000000000..f16fbb296 --- /dev/null +++ b/connector_jira/models/project_task/common.py @@ -0,0 +1,140 @@ +# -*- coding: utf-8 -*- +# Copyright 2016 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) + +from openerp import api, fields, models, exceptions, _ + +from ...unit.backend_adapter import JiraAdapter +from ...backend import jira + + +class JiraProjectTask(models.Model): + _name = 'jira.project.task' + _inherit = 'jira.binding' + _inherits = {'project.task': 'openerp_id'} + _description = 'Jira Tasks' + + openerp_id = fields.Many2one(comodel_name='project.task', + string='Task', + required=True, + index=True, + ondelete='restrict') + jira_key = fields.Char( + string='Key', + readonly=True, + ) + jira_issue_type_id = fields.Many2one( + comodel_name='jira.issue.type', + string='Issue Type', + readonly=True, + ) + jira_epic_link_id = fields.Many2one( + comodel_name='jira.project.task', + string='Epic', + readonly=True, + ) + jira_parent_id = fields.Many2one( + comodel_name='jira.project.task', + string='Parent Issue', + readonly=True, + help="Parent issue when the issue is a subtask. " + "Empty if the type of parent is filtered out " + "of the synchronizations.", + ) + + @api.multi + def unlink(self): + if any(self.mapped('external_id')): + raise exceptions.UserError( + _('A Jira task cannot be deleted.') + ) + return super(JiraProjectTask, self).unlink() + + +class ProjectTask(models.Model): + _inherit = 'project.task' + + jira_bind_ids = fields.One2many( + comodel_name='jira.project.task', + inverse_name='openerp_id', + copy=False, + string='Task Bindings', + context={'active_test': False}, + ) + jira_issue_type = fields.Char( + compute='_compute_jira_issue_type', + string='JIRA Issue Type', + store=True, + ) + jira_compound_key = fields.Char( + compute='_compute_jira_compound_key', + string='JIRA Key', + store=True, + ) + jira_epic_link_task_id = fields.Many2one( + comodel_name='project.task', + compute='_compute_jira_epic_link_task_id', + string='JIRA Epic', + store=True, + ) + jira_parent_task_id = fields.Many2one( + comodel_name='project.task', + compute='_compute_jira_parent_task_id', + string='JIRA Parent', + store=True, + ) + + @api.depends('jira_bind_ids.jira_issue_type_id.name') + def _compute_jira_issue_type(self): + for record in self: + types = record.mapped('jira_bind_ids.jira_issue_type_id.name') + record.jira_issue_type = ','.join([t for t in types if t]) + + @api.depends('jira_bind_ids.jira_key') + def _compute_jira_compound_key(self): + for record in self: + keys = record.mapped('jira_bind_ids.jira_key') + record.jira_compound_key = ','.join([k for k in keys if k]) + + @api.depends('jira_bind_ids.jira_epic_link_id.openerp_id') + def _compute_jira_epic_link_task_id(self): + for record in self: + tasks = record.mapped( + 'jira_bind_ids.jira_epic_link_id.openerp_id' + ) + if len(tasks) == 1: + record.jira_epic_link_task_id = tasks + + @api.depends('jira_bind_ids.jira_parent_id.openerp_id') + def _compute_jira_parent_task_id(self): + for record in self: + tasks = record.mapped( + 'jira_bind_ids.jira_parent_id.openerp_id' + ) + if len(tasks) == 1: + record.jira_parent_task_id = tasks + + @api.multi + def name_get(self): + names = [] + for task in self: + task_id, name = super(ProjectTask, task).name_get()[0] + if task.jira_compound_key: + name = '[%s] %s' % (task.jira_compound_key, name) + names.append((task_id, name)) + return names + + +@jira +class TaskAdapter(JiraAdapter): + _model_name = 'jira.project.task' + + def read(self, id_, fields=None): + return self.client.issue(id_, fields=fields).raw + + def search(self, jql): + # we need to have at least one field which is not 'id' or 'key' + # due to this bug: https://github.com/pycontribs/jira/pull/289 + fields = 'id,updated' + issues = self.client.search_issues(jql, fields=fields, maxResults=None) + return [issue.id for issue in issues] diff --git a/connector_jira/models/project_task/importer.py b/connector_jira/models/project_task/importer.py new file mode 100644 index 000000000..9086cfb90 --- /dev/null +++ b/connector_jira/models/project_task/importer.py @@ -0,0 +1,173 @@ +# -*- coding: utf-8 -*- +# Copyright 2016 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) + +from openerp import _ +from openerp.addons.connector.exception import MappingError +from openerp.addons.connector.unit.mapper import ImportMapper, mapping +from ...unit.backend_adapter import JiraAdapter +from ...unit.importer import ( + DelayedBatchImporter, + JiraImporter, + JiraDeleter, +) +from ...unit.mapper import FromFields +from ...backend import jira + + +@jira +class ProjectTaskMapper(ImportMapper, FromFields): + _model_name = 'jira.project.task' + + direct = [ + ('key', 'jira_key'), + ] + + from_fields = [ + ('summary', 'name'), + ('duedate', 'date_deadline'), + ] + + @mapping + def issue_type(self, record): + binder = self.binder_for('jira.issue.type') + jira_type_id = record['fields']['issuetype']['id'] + binding = binder.to_openerp(jira_type_id) + return {'jira_issue_type_id': binding.id} + + @mapping + def assignee(self, record): + assignee = record['fields'].get('assignee') + if not assignee: + return {'user_id': False} + jira_key = assignee['key'] + binder = self.binder_for('jira.res.users') + user = binder.to_openerp(jira_key, unwrap=True) + if not user: + email = assignee['emailAddress'] + raise MappingError( + _('No user found with login "%s" or email "%s".' + 'You must create a user or link it manually if the ' + 'login/email differs.') % (jira_key, email) + ) + return {'user_id': user.id} + + @mapping + def description(self, record): + # TODO: description is a variant of wiki syntax... + # and the Odoo field is HTML... + return {'description': record['fields']['description']} + + @mapping + def project(self, record): + jira_project_id = record['fields']['project']['id'] + binder = self.binder_for('jira.project.project') + project = binder.to_openerp(jira_project_id, unwrap=True) + return {'project_id': project.id} + + @mapping + def epic(self, record): + if not self.options.jira_epic: + return {} + jira_epic_id = self.options.jira_epic['id'] + binding = self.binder_for('jira.project.task').to_openerp(jira_epic_id) + return {'jira_epic_link_id': binding.id} + + @mapping + def parent(self, record): + jira_parent = record['fields'].get('parent', {}) + if not jira_parent: + return {} + jira_parent_id = jira_parent['id'] + binder = self.binder_for('jira.project.task') + binding = binder.to_openerp(jira_parent_id) + return {'jira_parent_id': binding.id} + + @mapping + def backend_id(self, record): + return {'backend_id': self.backend_record.id} + + +@jira +class ProjectTaskBatchImporter(DelayedBatchImporter): + """ Import the Jira tasks + + For every id in in the list of tasks, a delayed job is created. + Import from a date + """ + _model_name = 'jira.project.task' + + +@jira +class ProjectTaskImporter(JiraImporter): + _model_name = 'jira.project.task' + + def __init__(self, environment): + super(ProjectTaskImporter, self).__init__(environment) + self.jira_epic = None + + def _get_external_data(self): + """ Return the raw Jira data for ``self.external_id`` """ + result = super(ProjectTaskImporter, self)._get_external_data() + epic_field_name = self.backend_record.epic_link_field_name + if epic_field_name: + issue_adapter = self.unit_for(JiraAdapter, + model='jira.project.task') + epic_key = result['fields'][epic_field_name] + if epic_key: + self.jira_epic = issue_adapter.read(epic_key) + return result + + def _is_issue_type_sync(self): + jira_project_id = self.external_record['fields']['project']['id'] + binder = self.binder_for('jira.project.project') + project_binding = binder.to_openerp(jira_project_id) + task_sync_type_id = self.external_record['fields']['issuetype']['id'] + task_sync_type_binder = self.binder_for('jira.issue.type') + task_sync_type_binding = task_sync_type_binder.to_openerp( + task_sync_type_id, + ) + return task_sync_type_binding.is_sync_for_project(project_binding) + + def _create_data(self, map_record, **kwargs): + _super = super(ProjectTaskImporter, self) + return _super._create_data(map_record, jira_epic=self.jira_epic) + + def _update_data(self, map_record, **kwargs): + _super = super(ProjectTaskImporter, self) + return _super._update_data(map_record, jira_epic=self.jira_epic) + + def _import(self, binding, **kwargs): + # called at the beginning of _import because we must be sure + # that dependencies are there (project and issue type) + if not self._is_issue_type_sync(): + return _('Project or issue type is not synchronized.') + return super(ProjectTaskImporter, self)._import(binding, **kwargs) + + def _import_dependencies(self): + """ Import the dependencies for the record""" + jira_assignee = self.external_record['fields'].get('assignee') or {} + jira_key = jira_assignee.get('key') + self._import_dependency(jira_key, + 'jira.res.users', + record=jira_assignee) + + jira_issue_type = self.external_record['fields']['issuetype'] + jira_issue_type_id = jira_issue_type['id'] + self._import_dependency(jira_issue_type_id, 'jira.issue.type', + record=jira_issue_type) + + jira_parent = self.external_record['fields'].get('parent') + if jira_parent: + jira_parent_id = jira_parent['id'] + self._import_dependency(jira_parent_id, 'jira.project.task', + record=jira_parent) + + if self.jira_epic: + self._import_dependency(self.jira_epic['id'], 'jira.project.task', + record=self.jira_epic) + + +@jira +class ProjectTaskDeleter(JiraDeleter): + _model_name = 'jira.project.task' diff --git a/connector_jira/models/res_users/__init__.py b/connector_jira/models/res_users/__init__.py new file mode 100644 index 000000000..9d854de96 --- /dev/null +++ b/connector_jira/models/res_users/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- +from . import common +from . import importer diff --git a/connector_jira/models/res_users/common.py b/connector_jira/models/res_users/common.py new file mode 100644 index 000000000..cf3c35f7f --- /dev/null +++ b/connector_jira/models/res_users/common.py @@ -0,0 +1,90 @@ +# -*- coding: utf-8 -*- +# Copyright 2016 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) + + +from openerp import _, api, exceptions, fields, models +from openerp.addons.connector.connector import Binder + +from ...unit.backend_adapter import JiraAdapter +from ...backend import jira + + +class JiraResUsers(models.Model): + _name = 'jira.res.users' + _inherit = 'jira.binding' + _inherits = {'res.users': 'openerp_id'} + _description = 'Jira User' + + openerp_id = fields.Many2one(comodel_name='res.users', + string='User', + required=True, + index=True, + ondelete='restrict') + + +class ResUsers(models.Model): + _inherit = 'res.users' + + jira_bind_ids = fields.One2many( + comodel_name='jira.res.users', + inverse_name='openerp_id', + copy=False, + string='User Bindings', + context={'active_test': False}, + ) + + @api.multi + def button_link_with_jira(self): + self.ensure_one() + self.link_with_jira() + if not self.jira_bind_ids: + raise exceptions.UserError( + _('No JIRA user could be found') + ) + + @api.multi + def link_with_jira(self, backends=None): + if backends is None: + backends = self.env['jira.backend'].search([]) + for backend in backends: + with backend.get_environment('jira.res.users') as connector_env: + binder = connector_env.get_connector_unit(Binder) + adapter = connector_env.get_connector_unit(JiraAdapter) + for user in self: + if binder.to_backend(user, wrap=True): + continue + jira_user = adapter.search(fragment=user.email) + if not jira_user: + jira_user = adapter.search(fragment=user.login) + if not jira_user: + continue + elif len(jira_user) > 1: + raise exceptions.UserError( + _('Several users found for %s. ' + 'Set it manually..') % user.login + ) + jira_user, = jira_user + binding = self.env['jira.res.users'].create({ + 'backend_id': backend.id, + 'openerp_id': user.id, + }) + binder.bind(jira_user.key, binding) + + +@jira +class UserAdapter(JiraAdapter): + _model_name = 'jira.res.users' + + def read(self, id_): + return self.client.user(id_).raw + + def search(self, fragment=None): + """ Search users + + :param fragment: a string to match usernames, name or email against. + """ + users = self.client.search_users(fragment, maxResults=None, + includeActive=True, + includeInactive=True) + return users diff --git a/connector_jira/models/res_users/importer.py b/connector_jira/models/res_users/importer.py new file mode 100644 index 000000000..9886c7915 --- /dev/null +++ b/connector_jira/models/res_users/importer.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# Copyright 2016 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) + +from ...unit.importer import JiraImporter +from ...backend import jira + + +@jira +class UserImporter(JiraImporter): + _model_name = 'jira.res.users' + + def _import(self, binding): + record = self.external_record + jira_key = self.external_id + binder = self.binder_for('jira.res.users') + user = binder.to_openerp(jira_key, unwrap=True) + if not user: + email = record['emailAddress'] + user = self.env['res.users'].search( + ['|', + ('login', '=', jira_key), + ('email', '=', email)], + ) + user.link_with_jira(backends=self.backend_record) diff --git a/connector_jira/related_action.py b/connector_jira/related_action.py new file mode 100644 index 000000000..08a3cf0f6 --- /dev/null +++ b/connector_jira/related_action.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +# Copyright 2016 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) + +""" +Related Actions for Jira: + +Related actions are associated with jobs. +When called on a job, they will return an action to the client. + +""" + +import functools +from openerp.addons.connector import related_action +from .unit.binder import JiraBinder + +unwrap_binding = functools.partial(related_action.unwrap_binding, + binder_class=JiraBinder) diff --git a/connector_jira/security/ir.model.access.csv b/connector_jira/security/ir.model.access.csv new file mode 100644 index 000000000..017ed46cf --- /dev/null +++ b/connector_jira/security/ir.model.access.csv @@ -0,0 +1,16 @@ +"id","name","model_id:id","group_id:id","perm_read","perm_write","perm_create","perm_unlink" +"access_jira_backend","jira_backend connector manager","model_jira_backend","connector.group_connector_manager",1,1,1,1 +"access_jira_backend_user","jira_backend user","model_jira_backend","base.group_user",1,0,0,0 +"access_jira_backend_timestamp_user","jira_backend_timestamp user","model_jira_backend_timestamp","base.group_user",1,0,0,0 +"access_jira_backend_timestamp","jira_backend_timestamp connector manager","model_jira_backend_timestamp","connector.group_connector_manager",1,1,1,1 +"access_jira_project_project","jira_project_project connector manager","model_jira_project_project","connector.group_connector_manager",1,1,1,1 +"access_jira_project_project_manager","jira_project_project manager","model_jira_project_project","base.group_user",1,0,0,0 +"access_jira_project_project_user","jira_project_project user","model_jira_project_project","project.group_project_manager",1,1,1,1 +"access_jira_project_task","jira_project_task connector manager","model_jira_project_task","connector.group_connector_manager",1,1,1,1 +"access_jira_project_task_user","jira_project_task user","model_jira_project_task","base.group_user",1,0,0,0 +"access_jira_project_task_manager","jira_project_task manager","model_jira_project_task","project.group_project_manager",1,1,1,1 +"access_jira_res_users","jira_res_users connector manager","model_jira_res_users","connector.group_connector_manager",1,1,1,1 +"access_jira_res_users_user","jira_res_users user","model_jira_res_users","base.group_user",1,0,0,0 +"access_jira_res_users_manager","jira_res_users manager","model_jira_res_users","project.group_project_manager",1,1,1,1 +"access_jira_issue_type","jira_issue_type connector manager","model_jira_issue_type","connector.group_connector_manager",1,1,1,1 +"access_jira_issue_type_user","jira_issue_type user","model_jira_issue_type","base.group_user",1,0,0,0 diff --git a/connector_jira/unit/__init__.py b/connector_jira/unit/__init__.py new file mode 100644 index 000000000..e18346a53 --- /dev/null +++ b/connector_jira/unit/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +from . import backend_adapter +from . import binder +from . import exporter diff --git a/connector_jira/unit/backend_adapter.py b/connector_jira/unit/backend_adapter.py new file mode 100644 index 000000000..ba6ae5a59 --- /dev/null +++ b/connector_jira/unit/backend_adapter.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Copyright 2016 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) + + +from openerp.addons.connector.unit.backend_adapter import CRUDAdapter + +JIRA_JQL_DATETIME_FORMAT = '%Y-%m-%d %H:%M' # no seconds :-( + + +class JiraAdapter(CRUDAdapter): + """ External Records Adapter for Jira """ + + def __init__(self, environment): + """ + :param environment: current environment (backend, session, ...) + :type environment: :py:class:`connector.connector.Environment` + """ + super(JiraAdapter, self).__init__(environment) + self.client = self.backend_record.get_api_client() diff --git a/connector_jira/unit/binder.py b/connector_jira/unit/binder.py new file mode 100644 index 000000000..77c9fa587 --- /dev/null +++ b/connector_jira/unit/binder.py @@ -0,0 +1,68 @@ +# -*- coding: utf-8 -*- +# Copyright 2016 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) + +import logging + +from openerp import fields, models +from openerp.addons.connector.connector import Binder + +from ..backend import jira + +_logger = logging.getLogger(__name__) + + +@jira +class JiraBinder(Binder): + + _model_name = [ + 'jira.account.analytic.line', + 'jira.project.project', + 'jira.project.task', + 'jira.res.users', + ] + + def sync_date(self, binding): + assert self._sync_date_field + sync_date = binding[self._sync_date_field] + if not sync_date: + return + return fields.Datetime.from_string(sync_date) + + +@jira +class JiraModelBinder(Binder): + """ Binder for standalone models + + When we synchronize a model that has no equivalent + in Odoo, we create a model that hold the Jira records + without `_inherits`. + + """ + + _model_name = [ + 'jira.issue.type', + ] + + _openerp_field = 'id' + + def to_openerp(self, external_id, unwrap=False): + if unwrap: + _logger.warning('unwrap has no effect when the ' + 'binding is not an inherits ' + '(model %s)', self.model._name) + _super = super(JiraModelBinder, self) + return _super.to_openerp(external_id, unwrap=False) + + def unwrap_binding(self, binding_id, browse=False): + if isinstance(binding_id, models.BaseModel): + binding = binding_id + else: + binding = self.model.browse(binding_id) + if browse: + return binding + else: + return binding.id + + def unwrap_model(self): + return self.model diff --git a/connector_jira/unit/exporter.py b/connector_jira/unit/exporter.py new file mode 100644 index 000000000..f6b988423 --- /dev/null +++ b/connector_jira/unit/exporter.py @@ -0,0 +1,356 @@ +# -*- coding: utf-8 -*- +# Copyright 2016 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) + +import logging +from contextlib import contextmanager +import psycopg2 +from openerp import _, fields +from openerp.addons.connector.queue.job import job, related_action +from openerp.addons.connector.unit.synchronizer import Exporter +from openerp.addons.connector.exception import RetryableJobError +from .importer import import_record +from ..related_action import unwrap_binding +from .mapper import iso8601_to_utc_datetime + +_logger = logging.getLogger(__name__) + + +""" + +Exporters for Jira. + +In addition to its export job, an exporter has to: + +* check in Jira if the record has been updated more recently than the + last sync date and if yes, delay an import +* call the ``bind`` method of the binder to update the last sync date + +""" + + +class JiraBaseExporter(Exporter): + """ Base exporter for Jira """ + + def __init__(self, environment): + """ + :param environment: current environment (backend, session, ...) + :type environment: :py:class:`connector.connector.Environment` + """ + super(JiraBaseExporter, self).__init__(environment) + self.binding_id = None + self.external_id = None + + def _delay_import(self): + """ Schedule an import of the record. + + Adapt in the sub-classes when the model is not imported + using ``import_record``. + """ + # force is True because the sync_date will be more recent + # so the import would be skipped if it was not forced + assert self.external_id + import_record.delay(self.session, self.model._name, + self.backend_record.id, self.external_id, + force=True) + + def _should_import(self): + """ Before the export, compare the update date + in Jira and the last sync date in Odoo, + if the former is more recent, schedule an import + to not miss changes done in Jira. + """ + assert self.binding_record + if not self.external_id: + return False + sync = self.binder.sync_date(self.binding_record) + if not sync: + return True + jira_updated = self.backend_adapter.read( + self.external_id, + fields=['updated'] + )['fields']['updated'] + + sync_date = fields.Datetime.from_string(sync) + jira_date = iso8601_to_utc_datetime(jira_updated) + return sync_date < jira_date + + def _get_openerp_data(self): + """ Return the raw OpenERP data for ``self.binding_id`` """ + return self.model.browse(self.binding_id) + + def _lock(self): + """ Lock the binding record. + + Lock the binding record so we are sure that only one export + job is running for this record if concurrent jobs have to export the + same record. + + When concurrent jobs try to export the same record, the first one + will lock and proceed, the others will fail to lock and will be + retried later. + + This behavior works also when the export becomes multilevel + with :meth:`_export_dependencies`. Each level will set its own lock + on the binding record it has to export. + + Uses "NO KEY UPDATE", to avoid FK accesses + being blocked in PSQL > 9.3. + """ + sql = ("SELECT id FROM %s WHERE ID = %%s FOR NO KEY UPDATE NOWAIT" % + self.model._table) + try: + self.env.cr.execute(sql, (self.binding_id,), + log_exceptions=False) + except psycopg2.OperationalError: + _logger.info('A concurrent job is already exporting the same ' + 'record (%s with id %s). Job delayed later.', + self.model._name, self.binding_id) + raise RetryableJobError( + 'A concurrent job is already exporting the same record ' + '(%s with id %s). The job will be retried later.' % + (self.model._name, self.binding_id)) + + def run(self, binding_id, *args, **kwargs): + """ Run the synchronization + + :param binding_id: identifier of the binding record to export + """ + # prevent other jobs to export the same record + # will be released on commit (or rollback) + self._lock() + + self.binding_id = binding_id + self.binding_record = self._get_openerp_data() + + self.external_id = self.binder.to_backend(self.binding_id) + + result = self._run(*args, **kwargs) + + self.binder.bind(self.external_id, self.binding_id) + # commit so we keep the external ID if several exports + # are called and one of them fails + self.session.commit() + return result + + def _run(self, *args, **kwargs): + """ Flow of the synchronization, implemented in inherited classes""" + raise NotImplementedError + + +class JiraExporter(JiraBaseExporter): + """ A common flow for the exports to Jira """ + + def __init__(self, environment): + """ + :param environment: current environment (backend, session, ...) + :type environment: :py:class:`connector.connector.Environment` + """ + super(JiraExporter, self).__init__(environment) + self.binding_record = None + + def _has_to_skip(self): + """ Return True if the export can be skipped """ + return False + + @contextmanager + def _retry_unique_violation(self): + """ Context manager: catch Unique constraint error and retry the + job later. + + When we execute several jobs workers concurrently, it happens + that 2 jobs are creating the same record at the same time (binding + record created by :meth:`_export_dependency`), resulting in: + + IntegrityError: duplicate key value violates unique + constraint "jira_project_project_openerp_uniq" + DETAIL: Key (backend_id, openerp_id)=(1, 4851) already exists. + + In that case, we'll retry the import just later. + + """ + try: + yield + except psycopg2.IntegrityError as err: + if err.pgcode == psycopg2.errorcodes.UNIQUE_VIOLATION: + raise RetryableJobError( + 'A database error caused the failure of the job:\n' + '%s\n\n' + 'Likely due to 2 concurrent jobs wanting to create ' + 'the same record. The job will be retried later.' % err) + else: + raise + + def _export_dependency(self, relation, binding_model, + exporter_class=None): + """ + Export a dependency. The exporter class is a subclass of + ``JiraExporter``. If a more precise class need to be defined + + .. warning:: a commit is done at the end of the export of each + dependency. The reason for that is that we pushed a record + on the backend and we absolutely have to keep its ID. + + So you *must* take care to not modify the OpenERP database + excepted when writing back the external ID or eventual + external data to keep on this side. + + You should call this method only in the beginning of + exporter synchronization (in `~._export_dependencies`) + and do not write data which should be rollbacked in case + of error. + + :param relation: record to export if not already exported + :type relation: :py:class:`openerp.osv.orm.browse_record` + :param binding_model: name of the binding model for the relation + :type binding_model: str | unicode + :param exporter_cls: :py:class:`openerp.addons.connector.\ + connector.ConnectorUnit` + class or parent class to use for the export. + By default: JiraExporter + :type exporter_cls: :py:class:`openerp.addons.connector.\ + connector.MetaConnectorUnit` + """ + if not relation: + return + if exporter_class is None: + exporter_class = JiraExporter + rel_binder = self.binder_for(binding_model) + # wrap is typically True if the relation is a 'project.project' + # record but the binding model is 'jira.project.project' + wrap = relation._model._name != binding_model + + if wrap and hasattr(relation, 'jira_bind_ids'): + domain = [('openerp_id', '=', relation.id), + ('backend_id', '=', self.backend_record.id)] + model = self.env[binding_model].with_context(active_test=False) + binding = model.search(domain) + if binding: + binding.ensure_one() + else: + # we are working with a unwrapped record (e.g. + # product.template) and the binding does not exist yet. + # Example: I created a product.product and its binding + # jira.project.project, it is exported, but we need to + # create the binding for the template. + bind_values = {'backend_id': self.backend_record.id, + 'openerp_id': relation.id} + # If 2 jobs create it at the same time, retry + # one later. A unique constraint (backend_id, + # openerp_id) should exist on the binding model + with self._retry_unique_violation(): + model_c = self.env[binding_model].sudo().with_context( + connector_no_export=True + ) + binding = model_c.create(bind_values) + # Eager commit to avoid having 2 jobs + # exporting at the same time. + self.session.commit() + else: + # If jira_bind_ids does not exist we are typically in a + # "direct" binding (the binding record is the same record). + # If wrap is True, relation is already a binding record. + binding = relation + + if not rel_binder.to_backend(binding): + exporter = self.unit_for(exporter_class, binding_model) + exporter.run(binding.id) + + def _export_dependencies(self): + """ Export the dependencies for the record""" + return + + def _map_data(self, fields=None): + """ Returns an instance of + :py:class:`~openerp.addons.connector.unit.mapper.MapRecord` + + """ + return self.mapper.map_record(self.binding_record) + + def _validate_data(self, data): + """ Check if the values to import are correct + + Pro-actively check before the ``Model.create`` or + ``Model.update`` if some fields are missing + + Raise `InvalidDataError` + """ + return + + def _create_data(self, map_record, fields=None, **kwargs): + """ Get the data to pass to :py:meth:`_create`. + + Jira expect that we pass always all the fields, not only + the modified fields. That's why the `fields` argument + is None. + + """ + return map_record.values(for_create=True, fields=None, **kwargs) + + def _create(self, data): + """ Create the Jira record """ + self._validate_data(data) + return self.backend_adapter.create(data) + + def _update_data(self, map_record, fields=None, **kwargs): + """ Get the data to pass to :py:meth:`_update`. + + Jira expect that we pass always all the fields, not only + the modified fields. That's why the `fields` argument + is None. + + """ + return map_record.values(fields=None, **kwargs) + + def _update(self, data): + """ Update a Jira record """ + assert self.external_id + self._validate_data(data) + self.backend_adapter.write(self.external_id, data) + + def _run(self, fields=None): + """ Flow of the synchronization, implemented in inherited classes. + + `~._export_dependencies` might commit exported ids to the database, + so please do not do changes in the database before the export of the + dependencies because they won't be rollbacked. + """ + assert self.binding_id + assert self.binding_record + + if not self.external_id: + fields = None # should be created with all the fields + + if not self.binding_record.exists(): + return _('Record to export does no longer exist.') + + if self._has_to_skip(): + return + + # export the missing linked resources + self._export_dependencies() + + map_record = self._map_data(fields=fields) + + if self.external_id: + record = self._update_data(map_record, fields=fields) + if not record: + return _('Nothing to export.') + self._update(record) + else: + record = self._create_data(map_record, fields=fields) + if not record: + return _('Nothing to export.') + self.external_id = self._create(record) + return _('Record exported with ID %s on Jira.') % self.external_id + + +@job(default_channel='root.connector_jira.export') +@related_action(action=unwrap_binding) +def export_record(session, model_name, binding_id, fields=None): + """ Export a record on Jira """ + binding = session.env[model_name].browse(binding_id) + backend = binding.backend_id + with backend.get_environment(model_name, session=session) as connector_env: + exporter = connector_env.get_connector_unit(JiraBaseExporter) + return exporter.run(binding_id, fields=fields) diff --git a/connector_jira/unit/importer.py b/connector_jira/unit/importer.py new file mode 100644 index 000000000..6417e4195 --- /dev/null +++ b/connector_jira/unit/importer.py @@ -0,0 +1,450 @@ +# -*- coding: utf-8 -*- +# Copyright 2016 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) + +""" + +Importers for Jira. + +An import can be skipped if the last sync date is more recent than +the last update in Jira. + +They should call the ``bind`` method if the binder even if the records +are already bound, to update the last sync date. + +""" + +import logging +from contextlib import closing, contextmanager + +from psycopg2 import IntegrityError, errorcodes + +import openerp +from openerp import _ +from openerp.addons.connector.connector import Binder +from openerp.addons.connector.queue.job import job +from openerp.addons.connector.session import ConnectorSession +from openerp.addons.connector.unit.synchronizer import Importer, Deleter +from openerp.addons.connector.exception import (IDMissingInBackend, + RetryableJobError) +from .mapper import iso8601_to_utc_datetime +from .backend_adapter import JIRA_JQL_DATETIME_FORMAT + +_logger = logging.getLogger(__name__) + +RETRY_ON_ADVISORY_LOCK = 1 # seconds +RETRY_WHEN_CONCURRENT_DETECTED = 1 # seconds + + +class JiraImporter(Importer): + """ Base importer for Jira """ + + def __init__(self, environment): + """ + :param environment: current environment (backend, session, ...) + :type environment: :py:class:`connector.connector.Environment` + """ + super(JiraImporter, self).__init__(environment) + self.external_id = None + self.external_record = None + + def _get_external_data(self): + """ Return the raw Jira data for ``self.external_id`` """ + return self.backend_adapter.read(self.external_id) + + def must_skip(self): + """ Returns a reason if the import should be skipped. + + Returns None to continue with the import + + """ + assert self.external_record + return + + def _before_import(self): + """ Hook called before the import, when we have the Jira + data""" + + def _is_uptodate(self, binding): + """Return True if the import should be skipped because + it is already up-to-date in OpenERP""" + assert self.external_record + ext_fields = self.external_record.get('fields', {}) + external_updated_at = ext_fields.get('updated') + if not external_updated_at: + return False # no update date on Jira, always import it. + if not binding: + return # it does not exist so it should not be skipped + external_date = iso8601_to_utc_datetime(external_updated_at) + sync_date = self.binder.sync_date(binding) + if not sync_date: + return + # if the last synchronization date is greater than the last + # update in jira, we skip the import. + # Important: at the beginning of the exporters flows, we have to + # check if the jira date is more recent than the sync_date + # and if so, schedule a new import. If we don't do that, we'll + # miss changes done in Jira + return external_date < sync_date + + def _import_dependency(self, external_id, binding_model, + importer_class=None, record=None, always=False): + """ + Import a dependency. The importer class is a subclass of + ``JiraImporter``. A specific class can be defined. + + :param external_id: id of the related binding to import + :param binding_model: name of the binding model for the relation + :type binding_model: str | unicode + :param importer_cls: :py:class:`openerp.addons.connector.\ + connector.ConnectorUnit` + class or parent class to use for the export. + By default: JiraImporter + :type importer_cls: :py:class:`openerp.addons.connector.\ + connector.MetaConnectorUnit` + :param record: if we already have the data of the dependency, we + can pass it along to the dependency's importer + :type record: dict + :param always: if True, the record is updated even if it already + exists, + it is still skipped if it has not been modified on Jira + :type always: boolean + """ + if not external_id: + return + if importer_class is None: + importer_class = JiraImporter + binder = self.binder_for(binding_model) + if always or not binder.to_openerp(external_id): + importer = self.unit_for(importer_class, model=binding_model) + importer.run(external_id, record=record) + + def _import_dependencies(self): + """ Import the dependencies for the record""" + return + + def _map_data(self): + """ Returns an instance of + :py:class:`~openerp.addons.connector.unit.mapper.MapRecord` + + """ + return self.mapper.map_record(self.external_record) + + def _validate_data(self, data): + """ Check if the values to import are correct + + Pro-actively check before the ``_create`` or + ``_update`` if some fields are missing or invalid. + + Raise `InvalidDataError` + """ + return + + def _get_binding(self): + """Return the binding id from the jira id""" + return self.binder.to_openerp(self.external_id) + + def _create_data(self, map_record, **kwargs): + """ Get the data to pass to :py:meth:`_create` """ + return map_record.values(for_create=True, **kwargs) + + @contextmanager + def _retry_unique_violation(self): + """ Context manager: catch Unique constraint error and retry the + job later. + + When we execute several jobs workers concurrently, it happens + that 2 jobs are creating the same record at the same time + (especially product templates as they are shared by a lot of + sales orders), resulting in: + + IntegrityError: duplicate key value violates unique + constraint "jira_project_project_external_id_uniq" + DETAIL: Key (backend_id, external_id)=(1, 4851) already exists. + + In that case, we'll retry the import just later. + + """ + try: + yield + except IntegrityError as err: + if err.pgcode == errorcodes.UNIQUE_VIOLATION: + raise RetryableJobError( + 'A database error caused the failure of the job:\n' + '%s\n\n' + 'Likely due to 2 concurrent jobs wanting to create ' + 'the same record. The job will be retried later.' % err) + else: + raise + + def _create_context(self): + return { + 'connector_no_export': True + } + + def _create(self, data): + """ Create the Odoo record """ + # special check on data before import + self._validate_data(data) + with self._retry_unique_violation(): + model_ctx = self.model.with_context(**self._create_context()) + binding = model_ctx.create(data) + + _logger.debug('%s created from Jira %s', + binding, self.external_id) + return binding + + def _update_data(self, map_record, **kwargs): + """ Get the data to pass to :py:meth:`_update` """ + return map_record.values(**kwargs) + + def _update(self, binding, data): + """ Update an OpenERP record """ + # special check on data before import + self._validate_data(data) + binding.with_context(connector_no_export=True).write(data) + _logger.debug('%s updated from Jira %s', binding, self.external_id) + return + + def _after_import(self, binding): + """ Hook called at the end of the import """ + return + + @contextmanager + def do_in_new_connector_env(self, model_name=None): + """ Context manager that yields a new connector environment + + Using a new Odoo Environment thus a new PG transaction. + + This can be used to make a preemptive check in a new transaction, + for instance to see if another transaction already made the work. + """ + with openerp.api.Environment.manage(): + registry = openerp.modules.registry.RegistryManager.get( + self.env.cr.dbname + ) + with closing(registry.cursor()) as cr: + try: + new_env = openerp.api.Environment(cr, self.env.uid, + self.env.context) + new_connector_session = ConnectorSession.from_env(new_env) + connector_env = self.connector_env.create_environment( + self.backend_record.with_env(new_env), + new_connector_session, + model_name or self.model._name, + connector_env=self.connector_env + ) + yield connector_env + except: + cr.rollback() + raise + else: + cr.commit() + + def run(self, external_id, force=False, record=None, **kwargs): + """ Run the synchronization + + A record can be given, reducing number of calls when + a call already returns data (example: user returns addresses) + + :param external_id: identifier of the record on Jira + """ + self.external_id = external_id + lock_name = 'import({}, {}, {}, {})'.format( + self.backend_record._name, + self.backend_record.id, + self.model._name, + self.external_id, + ) + # Keep a lock on this import until the transaction is committed + self.advisory_lock_or_retry(lock_name, + retry_seconds=RETRY_ON_ADVISORY_LOCK) + if record is not None: + self.external_record = record + else: + try: + self.external_record = self._get_external_data() + except IDMissingInBackend: + return _('Record does no longer exist in Jira') + binding = self._get_binding() + if not binding: + with self.do_in_new_connector_env() as new_connector_env: + # Even when we use an advisory lock, we may have + # concurrent issues. + # Explanation: + # We import Partner A and B, both of them import a + # partner category X. + # + # The squares represent the duration of the advisory + # lock, the transactions starts and ends on the + # beginnings and endings of the 'Import Partner' + # blocks. + # T1 and T2 are the transactions. + # + # ---Time---> + # > T1 /------------------------\ + # > T1 | Import Partner A | + # > T1 \------------------------/ + # > T1 /-----------------\ + # > T1 | Imp. Category X | + # > T1 \-----------------/ + # > T2 /------------------------\ + # > T2 | Import Partner B | + # > T2 \------------------------/ + # > T2 /-----------------\ + # > T2 | Imp. Category X | + # > T2 \-----------------/ + # + # As you can see, the locks for Category X do not + # overlap, and the transaction T2 starts before the + # commit of T1. So no lock prevents T2 to import the + # category X and T2 does not see that T1 already + # imported it. + # + # The workaround is to open a new DB transaction at the + # beginning of each import (e.g. at the beginning of + # "Imp. Category X") and to check if the record has been + # imported meanwhile. If it has been imported, we raise + # a Retryable error so T2 is rollbacked and retried + # later (and the new T3 will be aware of the category X + # from the its inception). + binder = new_connector_env.get_connector_unit(Binder) + if binder.to_openerp(self.external_id): + raise RetryableJobError( + 'Concurrent error. The job will be retried later', + seconds=RETRY_WHEN_CONCURRENT_DETECTED, + ignore_retry=True + ) + + reason = self.must_skip() + if reason: + return reason + + if not force and self._is_uptodate(binding): + return _('Already up-to-date.') + + self._before_import() + + # import the missing linked resources + self._import_dependencies() + + self._import(binding, **kwargs) + + def _import(self, binding, **kwargs): + """ Import the external record. + + Can be inherited to modify for instance the session + (change current user, values in context, ...) + + """ + map_record = self._map_data() + + if binding: + record = self._update_data(map_record) + self._update(binding, record) + else: + record = self._create_data(map_record) + binding = self._create(record) + + with self._retry_unique_violation(): + self.binder.bind(self.external_id, binding) + + self._after_import(binding) + + +class BatchImporter(Importer): + """ The role of a BatchImporter is to search for a list of + items to import, then it can either import them directly or delay + the import of each item separately. + """ + + def run(self, from_date=None, to_date=None): + """ Run the synchronization """ + parts = [] + if from_date: + from_date = from_date.strftime(JIRA_JQL_DATETIME_FORMAT) + parts.append('updated >= "%s"' % from_date) + if to_date: + to_date = to_date.strftime(JIRA_JQL_DATETIME_FORMAT) + parts.append('updated <= "%s"' % to_date) + record_ids = self.backend_adapter.search(' and '.join(parts)) + for record_id in record_ids: + self._import_record(record_id) + + def _import_record(self, record_id): + """ Import a record directly or delay the import of the record. + + Method to implement in sub-classes. + """ + raise NotImplementedError + + +class DirectBatchImporter(BatchImporter): + """ Import the records directly, without delaying the jobs. """ + _model_name = None + + def _import_record(self, record_id): + """ Import the record directly """ + import_record(self.session, + self.model._name, + self.backend_record.id, + record_id) + + +class DelayedBatchImporter(BatchImporter): + """ Delay import of the records """ + _model_name = None + + def _import_record(self, record_id, **kwargs): + """ Delay the import of the records""" + import_record.delay(self.session, + self.model._name, + self.backend_record.id, + record_id, + **kwargs) + + +class JiraDeleter(Deleter): + _model_name = None + + def run(self, external_id, only_binding=False, set_inactive=False): + binding = self.binder.to_openerp(external_id) + if not binding.exists(): + return + if set_inactive: + binding.active = False + else: + record = binding.openerp_id + # emptying the external_id allows to unlink the binding + binding.external_id = False + binding.unlink() + if not only_binding: + record.unlink() + + +@job(default_channel='root.connector_jira.import') +def import_batch(session, model_name, backend_id, from_date=None, + to_date=None): + """ Prepare a batch import of records from Jira """ + backend = session.env['jira.backend'].browse(backend_id) + with backend.get_environment(model_name, session=session) as connector_env: + importer = connector_env.get_connector_unit(BatchImporter) + importer.run(from_date=from_date, to_date=to_date) + + +@job(default_channel='root.connector_jira.normal') +def import_record(session, model_name, backend_id, external_id, force=False): + """ Import a record from Jira """ + backend = session.env['jira.backend'].browse(backend_id) + with backend.get_environment(model_name, session=session) as connector_env: + importer = connector_env.get_connector_unit(JiraImporter) + importer.run(external_id, force=force) + + +@job(default_channel='root.connector_jira.normal') +def delete_record(session, model_name, backend_id, external_id): + """ Delete a local record which has been deleted on JIRA """ + backend = session.env['jira.backend'].browse(backend_id) + with backend.get_environment(model_name, session=session) as connector_env: + deleter = connector_env.get_connector_unit(JiraDeleter) + deleter.run(external_id) diff --git a/connector_jira/unit/mapper.py b/connector_jira/unit/mapper.py new file mode 100644 index 000000000..59620e744 --- /dev/null +++ b/connector_jira/unit/mapper.py @@ -0,0 +1,160 @@ +# -*- coding: utf-8 -*- +# Copyright 2016 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) + +import pytz +from datetime import datetime +from dateutil import parser + +from openerp import fields +from openerp.addons.connector.unit import mapper + + +def iso8601_to_utc_datetime(isodate): + """ Returns the UTC datetime from an iso8601 date + + A JIRA date is formatted using the ISO 8601 format. + Example: 2013-11-04T13:52:01+0100 + """ + parsed = parser.parse(isodate) + if not parsed.tzinfo: + return parsed + utc = pytz.timezone('UTC') + # set as UTC and then remove the tzinfo so the date becomes naive + return parsed.astimezone(utc).replace(tzinfo=None) + + +def utc_datetime_to_iso8601(dt): + """ Returns an iso8601 datetime from a datetime. + + Example: 2013-11-04 12:52:01 → 2013-11-04T12:52:01+0000 + + """ + utc = pytz.timezone('UTC') + utc_dt = utc.localize(dt, is_dst=False) # UTC = no DST + return utc_dt.isoformat() + + +def iso8601_to_utc(field): + """ A modifier intended to be used on the ``direct`` mappings for + importers. + + A Jira date is formatted using the ISO 8601 format. + Convert an ISO 8601 timestamp to an UTC datetime as string + as expected by Odoo. + + Example: 2013-11-04T13:52:01+0100 -> 2013-11-04 12:52:01 + + Usage:: + + direct = [(iso8601_to_utc('date_field'), 'date_field')] + + :param field: name of the source field in the record + + """ + def modifier(self, record, to_attr): + value = record.get(field) + if not value: + return False + utc_date = iso8601_to_utc_datetime(value) + return fields.Datetime.to_string(utc_date) + return modifier + + +def iso8601_to_local_date(isodate): + """ Returns the local date from an iso8601 date + + Keep only the date, when we want to keep only the local date. + It's safe to extract it directly from the tz-aware timestamp. + Example with 2014-10-07T00:34:59+0200: we want 2014-10-07 and not + 2014-10-06 that we would have using the timestamp converted to UTC. + """ + local_date = isodate[:10] + return datetime.strptime(local_date, '%Y-%m-%d').date() + + +def iso8601_local_date(field): + """ A modifier intended to be used on the ``direct`` mappings for + importers. + + A JIRA datetime is formatted using the ISO 8601 format. + Returns the local date from an iso8601 datetime. + + Keep only the date, when we want to keep only the local date. + It's safe to extract it directly from the tz-aware timestamp. + Example with 2014-10-07T00:34:59+0200: we want 2014-10-07 and not + 2014-10-06 that we would have using the timestamp converted to UTC. + + Usage:: + + direct = [(iso8601_local_date('name'), 'name')] + + :param field: name of the source field in the record + + """ + def modifier(self, record, to_attr): + value = record.get(field) + if not value: + return False + utc_date = iso8601_to_local_date(value) + return fields.Date.to_string(utc_date) + return modifier + + +def follow_dict_path(field): + """A modifier intended to be used on ``direct`` mappings. + + 'Follows' children keys in dictionaries + If a key is missing along the path, ``None`` is returned. + + Examples: + Assuming a dict `{'a': {'b': 1}} + + direct = [ + (follow_dict_path('a.b'), 'cat')] + + Then 'cat' will be 1. + + :param field: field "path", using dots for subkeys + """ + def modifier(self, record, to_attr): + attrs = field.split('.') + value = record + for attr in attrs: + value = value.get(attr) + if not value: + break + return value + return modifier + + +def whenempty(field, default_value): + """ Set a default value when the value is evaluated to False + + A modifier intended to be used on the ``direct`` mappings. + + Example:: + + direct = [(whenempty('source', 'default value'), 'target')] + + :param field: name of the source field in the record + :param default_value: value to set when the source value is False-ish + """ + def modifier(self, record, to_attr): + value = record[field] + if not value: + return default_value + return value + return modifier + + +class FromFields(mapper.Mapper): + + @mapper.mapping + def values_from_attributes(self, record): + values = {} + from_fields_mappings = getattr(self, 'from_fields', []) + fields_values = record.get('fields', {}) + for source, target in from_fields_mappings: + values[target] = self._map_direct(fields_values, source, target) + return values diff --git a/connector_jira/views/jira_backend_views.xml b/connector_jira/views/jira_backend_views.xml new file mode 100644 index 000000000..e1727a867 --- /dev/null +++ b/connector_jira/views/jira_backend_views.xml @@ -0,0 +1,209 @@ + + + + + + jira.backend.form + jira.backend + +
+
+
+ +
+
+ jira.project.project.tree jira.project.project - + @@ -71,4 +84,17 @@ + + project.project.kanban.jira + project.project + + + + + + + +
diff --git a/connector_jira/views/timesheet_account_analytic_line.xml b/connector_jira/views/timesheet_account_analytic_line.xml index eb0dccf89..81be593f4 100644 --- a/connector_jira/views/timesheet_account_analytic_line.xml +++ b/connector_jira/views/timesheet_account_analytic_line.xml @@ -9,10 +9,12 @@ - - @@ -28,7 +30,7 @@ - Date: Tue, 29 Jan 2019 15:50:25 +0100 Subject: [PATCH 009/113] Add connector_jira_servicedesk Map projects by external_id + set of jira orgs Project bindings now can be assigned to one or more jira organizations. The binding for the project accept an additional argument for organizations. A task will be linked with the project having the exact same set of organizations that it has, or fallback to a project without organization. A constraint ensures that you cannot have several projects with the same set of organizations or 2 projects without organization. The link wizard has a new step to select the organization. The REST API for Serviced Desk is a different one. The former code was based on https://github.com/pycontribs/jira/pull/388 which is closed and unmaintained. We only need to read the organizations from the servicedesk REST API and the local code is minimal. We can now use the normal jira library. --- connector_jira/__manifest__.py | 2 +- .../models/account_analytic_line/importer.py | 60 ++++++++++++++----- .../models/project_project/common.py | 43 +++++++++++-- .../project_project/project_link_jira.py | 2 +- .../models/project_task/importer.py | 40 ++++++++++--- 5 files changed, 119 insertions(+), 28 deletions(-) diff --git a/connector_jira/__manifest__.py b/connector_jira/__manifest__.py index 578101e8b..16038e87b 100644 --- a/connector_jira/__manifest__.py +++ b/connector_jira/__manifest__.py @@ -25,7 +25,7 @@ 'cryptography', ], }, - 'website': 'https://www.camptocamp.com', + 'website': 'https://github.com/camptocamp/connector-jira', 'data': [ 'views/jira_menus.xml', 'wizards/jira_backend_auth_views.xml', diff --git a/connector_jira/models/account_analytic_line/importer.py b/connector_jira/models/account_analytic_line/importer.py index 380bc376b..ba28a82fb 100644 --- a/connector_jira/models/account_analytic_line/importer.py +++ b/connector_jira/models/account_analytic_line/importer.py @@ -1,6 +1,8 @@ -# Copyright 2016 Camptocamp SA +# Copyright 2016-2019 Camptocamp SA # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) +import logging + from odoo import _ from odoo.addons.connector.exception import MappingError from odoo.addons.connector.components.mapper import mapping, only_create @@ -8,6 +10,8 @@ from ...components.backend_adapter import JIRA_JQL_DATETIME_FORMAT from ...components.mapper import iso8601_local_date, whenempty +_logger = logging.getLogger(__name__) + class AnalyticLineMapper(Component): _name = 'jira.analytic.line.mapper' @@ -47,21 +51,19 @@ def author(self, record): 'You must create a user or link it manually if the ' 'login/email differs.') % (jira_author_key, email) ) - return {'user_id': user.id} + employee = self.env['hr.employee'].search( + [('user_id', '=', user.id)], + limit=1 + ) + return {'user_id': user.id, 'employee_id': employee.id} @mapping def project_and_task(self, record): + assert self.options.task_binding or self.options.project_binding task_binding = self.options.task_binding - if not task_binding: - issue = self.options.linked_issue - assert issue - project_binder = self.binder_for('jira.project.project') - jira_project_id = issue['fields']['project']['id'] - project = project_binder.to_internal(jira_project_id, unwrap=True) - # we can link to any task so we create the worklog - # on the project without any task - return {'account_id': project.analytic_account_id.id} + project = self.options.project_binding.odoo_id + return {'project_id': project.id} project = task_binding.project_id return {'task_id': task_binding.odoo_id.id, @@ -116,6 +118,12 @@ def __init__(self, work_context): super().__init__(work_context) self.external_issue_id = None self.task_binding = None + self.project_binding = None + + @property + def _issue_fields_to_read(self): + epic_field_name = self.backend_record.epic_link_field_name + return ['issuetype', 'project', 'parent', epic_field_name] def _recurse_import_task(self): """ Import and return the task of proper type for the worklog @@ -130,20 +138,21 @@ def _recurse_import_task(self): """ issue_adapter = self.component(usage='backend.adapter', model_name='jira.project.task') - project_binder = self.binder_for('jira.project.project') issue_binder = self.binder_for('jira.project.task') issue_type_binder = self.binder_for('jira.issue.type') jira_issue_id = self.external_record['issueId'] epic_field_name = self.backend_record.epic_link_field_name + project_matcher = self.component(usage='jira.task.project.matcher') current_project_id = self.external_issue['fields']['project']['id'] while jira_issue_id: issue = issue_adapter.read( jira_issue_id, - fields=['issuetype', 'project', 'parent', epic_field_name], + fields=self._issue_fields_to_read, ) + jira_project_id = issue['fields']['project']['id'] jira_issue_type_id = issue['fields']['issuetype']['id'] - project_binding = project_binder.to_internal(jira_project_id) + project_binding = project_matcher.find_project_binding(issue) issue_type_binding = issue_type_binder.to_internal( jira_issue_type_id ) @@ -174,11 +183,13 @@ def _recurse_import_task(self): def _create_data(self, map_record, **kwargs): return super()._create_data(map_record, task_binding=self.task_binding, + project_binding=self.project_binding, linked_issue=self.external_issue) def _update_data(self, map_record, **kwargs): return super()._update_data(map_record, task_binding=self.task_binding, + project_binding=self.project_binding, linked_issue=self.external_issue) def run(self, external_id, force=False, record=None, **kwargs): @@ -198,9 +209,28 @@ def _get_external_data(self): return self.backend_adapter.read(self.external_issue_id, self.external_id) + def _before_import(self): + self.task_binding = self._recurse_import_task() + if not self.task_binding: + # when no task exists in Odoo (because we don't synchronize + # the issue type for instance), we link the line directly + # to the corresponding project, not linked to any task + issue = self.external_issue + assert issue + matcher = self.component(usage='jira.task.project.matcher') + self.project_binding = matcher.find_project_binding(issue) + + def _import(self, binding, **kwargs): + if not self.task_binding and not self.project_binding: + _logger.debug( + "No task or project synchronized for attaching worklog %s", + self.external_record['id'] + ) + return + return super()._import(binding, **kwargs) + def _import_dependencies(self): """ Import the dependencies for the record""" - self.task_binding = self._recurse_import_task() jira_assignee = self.external_record['author'] jira_key = jira_assignee.get('key') self._import_dependency(jira_key, diff --git a/connector_jira/models/project_project/common.py b/connector_jira/models/project_project/common.py index bb94eb668..73b79db3b 100644 --- a/connector_jira/models/project_project/common.py +++ b/connector_jira/models/project_project/common.py @@ -9,7 +9,7 @@ from jira import JIRAError from jira.utils import json_loads -from odoo import api, fields, models, exceptions, _ +from odoo import api, fields, models, exceptions, _, tools from odoo.addons.component.core import Component @@ -22,7 +22,7 @@ class JiraProjectBaseFields(models.AbstractModel): Shared by the binding jira.project.project and the wizard to link/create a JIRA project """ - _name = 'jira.project.base.fields' + _name = 'jira.project.base.mixin' sync_issue_type_ids = fields.Many2many( comodel_name='jira.issue.type', @@ -63,7 +63,7 @@ def _selection_project_template(self): class JiraProjectProject(models.Model): _name = 'jira.project.project' - _inherit = ['jira.binding', 'jira.project.base.fields'] + _inherit = ['jira.binding', 'jira.project.base.mixin'] _inherits = {'project.project': 'odoo_id'} _description = 'Jira Projects' @@ -78,6 +78,42 @@ class JiraProjectProject(models.Model): "A binding already exists for this project and this backend."), ] + # Disable and implement the constraint jira_binding_uniq as python because + # we need to override the in connector_jira_service_desk and it would try + # to create it again at every update because of the base implementation + # in the binding's parent model. + @api.model_cr + def _add_sql_constraints(self): + # we replace the sql constraint by a python one + # to include the organizations + constraints = [] + for (key, definition, msg) in self._sql_constraints: + if key == 'jira_binding_uniq': + conname = '%s_%s' % (self._table, key) + has_definition = tools.constraint_definition( + self.env.cr, conname + ) + if has_definition: + tools.drop_constraint(self.env.cr, self._table, conname) + else: + constraints.append((key, definition, msg)) + self._sql_constraints = constraints + super()._add_sql_constraints() + + @api.constrains('backend_id', 'external_id') + def _constrains_jira_uniq(self): + for binding in self: + same_link_bindings = self.search([ + ('id', '!=', self.id), + ('backend_id', '=', self.backend_id.id), + ('external_id', '=', self.external_id), + ]) + if same_link_bindings: + raise exceptions.ValidationError(_( + "The project %s is already linked with the same" + " JIRA project." + ) % (same_link_bindings.display_name)) + @api.onchange('backend_id') def onchange_project_backend_id(self): self.project_template = self.backend_id.project_template @@ -104,7 +140,6 @@ def create(self, values): record._ensure_jira_key() return record - @api.multi def write(self, values): if 'project_template' in values: diff --git a/connector_jira/models/project_project/project_link_jira.py b/connector_jira/models/project_project/project_link_jira.py index 0be8b7351..7fd72f395 100644 --- a/connector_jira/models/project_project/project_link_jira.py +++ b/connector_jira/models/project_project/project_link_jira.py @@ -12,7 +12,7 @@ class ProjectLinkJira(models.TransientModel): _name = 'project.link.jira' - _inherit = 'jira.project.base.fields' + _inherit = 'jira.project.base.mixin' _description = 'Link Project with JIRA' project_id = fields.Many2one( diff --git a/connector_jira/models/project_task/importer.py b/connector_jira/models/project_task/importer.py index 2514f975b..c309ea32d 100644 --- a/connector_jira/models/project_task/importer.py +++ b/connector_jira/models/project_task/importer.py @@ -59,9 +59,8 @@ def description(self, record): @mapping def project(self, record): - jira_project_id = record['fields']['project']['id'] binder = self.binder_for('jira.project.project') - project = binder.to_internal(jira_project_id, unwrap=True) + project = binder.unwrap_binding(self.options.project_binding) return {'project_id': project.id} @mapping @@ -99,6 +98,17 @@ class ProjectTaskBatchImporter(Component): _apply_on = ['jira.project.task'] +class ProjectTaskProjectMatcher(Component): + _name = 'jira.task.project.matcher' + _inherit = ['jira.base'] + _usage = 'jira.task.project.matcher' + + def find_project_binding(self, jira_task_data, unwrap=False): + jira_project_id = self.external_record['fields']['project']['id'] + binder = self.binder_for('jira.project.project') + return binder.to_internal(jira_project_id, unwrap=unwrap) + + class ProjectTaskImporter(Component): _name = 'jira.project.task.importer' _inherit = ['jira.importer'] @@ -107,6 +117,7 @@ class ProjectTaskImporter(Component): def __init__(self, work_context): super().__init__(work_context) self.jira_epic = None + self.project_binding = None def _get_external_data(self): """ Return the raw Jira data for ``self.external_id`` """ @@ -122,10 +133,14 @@ def _get_external_data(self): self.jira_epic = issue_adapter.read(epic_key) return result + def _find_project_binding(self): + matcher = self.component(usage='jira.task.project.matcher') + self.project_binding = matcher.find_project_binding( + self.external_record + ) + def _is_issue_type_sync(self): - jira_project_id = self.external_record['fields']['project']['id'] - binder = self.binder_for('jira.project.project') - project_binding = binder.to_internal(jira_project_id) + project_binding = self.project_binding task_sync_type_id = self.external_record['fields']['issuetype']['id'] task_sync_type_binder = self.binder_for('jira.issue.type') task_sync_type_binding = task_sync_type_binder.to_internal( @@ -134,14 +149,25 @@ def _is_issue_type_sync(self): return task_sync_type_binding.is_sync_for_project(project_binding) def _create_data(self, map_record, **kwargs): - return super()._create_data(map_record, jira_epic=self.jira_epic) + return super()._create_data( + map_record, + jira_epic=self.jira_epic, + project_binding=self.project_binding, + **kwargs + ) def _update_data(self, map_record, **kwargs): - return super()._update_data(map_record, jira_epic=self.jira_epic) + return super()._update_data( + map_record, + jira_epic=self.jira_epic, + project_binding=self.project_binding, + **kwargs + ) def _import(self, binding, **kwargs): # called at the beginning of _import because we must be sure # that dependencies are there (project and issue type) + self._find_project_binding() if not self._is_issue_type_sync(): return _('Project or issue type is not synchronized.') return super()._import(binding, **kwargs) From ef8e174b776fb37ca4ccaf158ddf4366ef58512f Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Fri, 1 Feb 2019 09:59:29 +0100 Subject: [PATCH 010/113] Read access tokens with sudo A normal user must be able to use the jira rest client under the hood. Update documentation Store original jira issues on analytic lines Instead of the issue they are linked to in Odoo. With the mechanism in place, if we don't import the task or bug issue types and we synchronize the epics, the worklogs will be attached to the epics of their tasks (of subtasks to tasks). The fields were showing the values of the Epic (or task for subtask), though it makes much more sense to keep the keys and URLs of the original issue and epic on the analytic lines. We still have the link to the task if we want to get the URL for the task they are currently linked to. Fix project binding unique constraint We should be able to have 2 bindings without external_id (not yet exported) Add wizard to link a task to JIRA Refactorize multistep wizards with a mixin Handle jira bindings in tasks merge wizard Import name of Epics instead of summary The Epic issues have a special (custom) field for their name. In fact they use both, the custom field AND the summary field. But I guess they are better identified by their epic name than their summary. Extract multi_step_wizard as an addon Set a timeout on requests to JIRA Fix typo in readme Fix backend selection on wizard to link a task The computed field was not called, due to the model being a TransientModel, replace it by default values. Add is_master on jira project bindings Allow to have one project binding per project type The unicity constraint (backend_id, odoo_id) on jira.backend.backend is relaxed: it now allows one binding of each type. The reason for this is: * supporting several projects of different types is a requirements (eg. 1 service desk and 1 software) * but if we implement new features like "if I create a task it is pushed to Jira", with different projects we would not know where to push them Using this constraint, we'll be able to focus new export features by project type. Move jira_key from project to jira project binding As we can have more than one project binding, we cannot store a single jira key. In case we have more than one jira key for a project, we compute a compound key joined by commas. Add method to handle jira API / connection errors Add migration for 11.0.1.1.0 Fix error when several odoo users match When we are importing a worklog for a user not yet linked and we found several candidate users in odoo, they will both be linked with the same jira user and make the unicity constraint fail. Properly raise an error in this case. Fix project_type as string Add fallback project for worklogs A new optional field on the backend allows to choose a fallback project for the worklogs. When a worklog doesn't match any project linked with Jira, they will be created there, allowing to find the misconfigurations and fix them. Add action to reimport worklogs from Jira This is meant mainly to be used when a worklog has been imported in the fallback project and we need to re-affect them to the correct project after we linked it. Add related action to open jira with import issues The 'related' button on jobs which import issues or worklog will now open jira directly on the issue. Skip import of tasks/worklogs before batch dates Jira may send webhooks for old records, for instance worklogs from 6 month ago because their task has been changed. This change ensures that we never import any record which has a last update date before the last date of batch import. The job methods now return the result of Importer.run() so the result is shown on the jobs in the UI. Extract import of dependencies Allowing to override partially the dependencies to import Fix import of subtasks When importing the parent of a subtask, the record in the 'parent' field (the task data) is incomplete, it contains only a few fields. Providing only the id to the dependency Importer will force it to read the whole record from Jira. Prevent duplicates with inactive projects The constraint did not look for inactive projects, which makes the import fail later because it finds several projects for the same task. Handle 404 errors when importing records When a record does not exist on Jira: * the job is done instead of failed * a result on the job tells about the missing record * the binding is deleted on Odoo * for worklogs, the analytic line is deleted as well Change xmlid for model_account_analytic_line account is not necessarily installed, while analytic is installed by the project module in any case Partial revert of 2fa7d39 Commit title was: Skip import of tasks/worklogs before batch dates The process is the following: * Read T from backend, T is the is the last time we ran the import * Create a batch job to import tasks/worklogs from T to now() and update the backend with now() The batch job is run asynchronously, it generates one job per task or worklog to import between T and now(). When the jobs are executed, the last batch date has already been updated to now(), so the jobs would never import any record. Fix error when force_reimport called on several records Prevent changing task's project on binding When the task Mapper binds a task to a task already existing in Odoo, we must not change the project_id of the task. This is not possible as long as we have invoiced timesheet lines, even if the id is the same than the current one. Add base for tests and a test for oauth Add test for check of connection With a fix in the method: getting the client only wasn't triggering any error, calling 'myself' raises a 401 when we are not authenticated. Add documentation for tests Add travis configuration Add tests on issue types and tasks Add first test for analytic account lines Make pylint-odoo happy Add a script to initiate the Oauth dance --- connector_jira/README.rst | 57 +- connector_jira/__init__.py | 1 + connector_jira/__manifest__.py | 12 +- connector_jira/cli/__init__.py | 1 + connector_jira/cli/jira_oauth_dance.py | 146 ++ connector_jira/components/backend_adapter.py | 57 +- connector_jira/components/base.py | 2 +- connector_jira/components/binder.py | 2 +- connector_jira/components/exporter.py | 6 +- connector_jira/components/importer.py | 47 +- connector_jira/components/mapper.py | 2 +- connector_jira/controllers/main.py | 2 +- connector_jira/demo/jira_backend_demo.xml | 9 + .../migrations/11.0.1.1.0/pre-migration.py | 41 + connector_jira/models/__init__.py | 1 + .../models/account_analytic_line/common.py | 107 +- .../models/account_analytic_line/importer.py | 67 +- connector_jira/models/jira_backend/common.py | 71 +- connector_jira/models/jira_binding/common.py | 11 +- .../models/jira_issue_type/common.py | 5 +- .../models/jira_issue_type/importer.py | 2 +- .../models/project_project/__init__.py | 1 + .../models/project_project/binder.py | 50 + .../models/project_project/common.py | 143 +- .../models/project_project/exporter.py | 2 +- .../project_project/project_link_jira.py | 82 +- .../models/project_task/__init__.py | 1 + connector_jira/models/project_task/common.py | 25 +- .../models/project_task/importer.py | 52 +- .../models/project_task/task_link_jira.py | 96 + connector_jira/models/queue_job/__init__.py | 1 + connector_jira/models/queue_job/common.py | 39 + connector_jira/models/res_users/common.py | 5 +- connector_jira/models/res_users/importer.py | 16 +- connector_jira/tests/__init__.py | 4 + connector_jira/tests/common.py | 272 +++ .../cassettes/test_auth_check_connection.yaml | 295 +++ .../test_auth_check_connection_failure.yaml | 355 ++++ .../fixtures/cassettes/test_auth_oauth.yaml | 76 + .../test_import_issue_type_batch.yaml | 1766 +++++++++++++++++ .../cassettes/test_import_task_epic.yaml | 308 +++ .../cassettes/test_import_task_parents.yaml | 923 +++++++++ .../test_import_task_type_not_synced.yaml | 308 +++ .../cassettes/test_import_worklog.yaml | 898 +++++++++ connector_jira/tests/test_auth.py | 70 + .../test_import_account_analytic_line.py | 66 + .../tests/test_import_issue_type.py | 41 + connector_jira/tests/test_import_task.py | 103 + connector_jira/views/jira_backend_views.xml | 34 +- .../views/project_link_jira_views.xml | 16 +- .../views/project_project_views.xml | 7 +- connector_jira/views/project_task_views.xml | 7 + connector_jira/views/task_link_jira_views.xml | 36 + .../views/timesheet_account_analytic_line.xml | 39 +- connector_jira/wizards/__init__.py | 2 + .../jira_account_analytic_line_import.py | 26 + ...ira_account_analytic_line_import_views.xml | 33 + connector_jira/wizards/jira_backend_auth.py | 19 +- .../wizards/jira_backend_auth_views.xml | 2 +- .../wizards/project_task_merge_wizard.py | 25 + 60 files changed, 6634 insertions(+), 259 deletions(-) create mode 100644 connector_jira/cli/__init__.py create mode 100644 connector_jira/cli/jira_oauth_dance.py create mode 100644 connector_jira/demo/jira_backend_demo.xml create mode 100644 connector_jira/migrations/11.0.1.1.0/pre-migration.py create mode 100644 connector_jira/models/project_project/binder.py create mode 100644 connector_jira/models/project_task/task_link_jira.py create mode 100644 connector_jira/models/queue_job/__init__.py create mode 100644 connector_jira/models/queue_job/common.py create mode 100644 connector_jira/tests/__init__.py create mode 100644 connector_jira/tests/common.py create mode 100644 connector_jira/tests/fixtures/cassettes/test_auth_check_connection.yaml create mode 100644 connector_jira/tests/fixtures/cassettes/test_auth_check_connection_failure.yaml create mode 100644 connector_jira/tests/fixtures/cassettes/test_auth_oauth.yaml create mode 100644 connector_jira/tests/fixtures/cassettes/test_import_issue_type_batch.yaml create mode 100644 connector_jira/tests/fixtures/cassettes/test_import_task_epic.yaml create mode 100644 connector_jira/tests/fixtures/cassettes/test_import_task_parents.yaml create mode 100644 connector_jira/tests/fixtures/cassettes/test_import_task_type_not_synced.yaml create mode 100644 connector_jira/tests/fixtures/cassettes/test_import_worklog.yaml create mode 100644 connector_jira/tests/test_auth.py create mode 100644 connector_jira/tests/test_import_account_analytic_line.py create mode 100644 connector_jira/tests/test_import_issue_type.py create mode 100644 connector_jira/tests/test_import_task.py create mode 100644 connector_jira/views/task_link_jira_views.xml create mode 100644 connector_jira/wizards/jira_account_analytic_line_import.py create mode 100644 connector_jira/wizards/jira_account_analytic_line_import_views.xml create mode 100644 connector_jira/wizards/project_task_merge_wizard.py diff --git a/connector_jira/README.rst b/connector_jira/README.rst index 353ca45bf..d7a385e87 100644 --- a/connector_jira/README.rst +++ b/connector_jira/README.rst @@ -25,10 +25,10 @@ Backend 1. Open the menu Connectors > Jira > Backends 2. Create a new Jira Backend -* Put the name you want -* Set the URL of your Jira, like https://jira.example.com -* You can also select the company where records will be created and the - default project template used when Odoo will create the projects in Jira + * Put the name you want + * Set the URL of your Jira, like https://jira.example.com + * You can also select the company where records will be created and the + default project template used when Odoo will create the projects in Jira 3. Save and continue with the Authentication @@ -79,8 +79,11 @@ Configuration done You can now click on the button "Configuration Done". -Syncronizations -^^^^^^^^^^^^^^^ +Synchronizations +^^^^^^^^^^^^^^^^ + +The tasks and worklogs are always imported from JIRA to Odoo, there +is no synchronization in the other direction. Initial synchronizations """""""""""""""""""""""" @@ -91,16 +94,41 @@ users" and "Import issue types". The users will be matched either by login or by Create and export a project """"""""""""""""""""""""""" -Projects are created in Odoo and exported to Jira. You can then create a -project, set a Jira key. -Then, open the Connectors tab, add a new link. -You can chose the Jira to export to, and the type of project. -You have to select the synchronized issue types, but the field is not editable -until the record is saved. You need to save and edit it again to be able to -select them. +Projects can be created in Odoo and exported to Jira. You can then create a +project, and use the action "Link with JIRA" and use the "Export to JIRA" action. + +When you choose to export a project to JIRA, if you change the name +or the key of the project, the new values will be pushed to JIRA. + +Link a project with JIRA +"""""""""""""""""""""""" + +If you already have a project on JIRA or prefer to create it first on JIRA, +you can link an Odoo project. Use the "Link with JIRA" action on the project +and select the "Link with JIRA" action. + +This action establish the link, then changes of the name or the key on either +side are not pushed. + +Issue Types on Projects +""""""""""""""""""""""" + +When you link a project, you have to select which issue types are synchronized. +Only tasks of the selected types will be created in Odoo. + +If a JIRA worklog is added to a type of issue that is not synchronized, +will attach to the closest task following these rules: + +* if a subtask, find the parent task +* if no parent task, find the epic task (only if it is on the same project) +* if no epic, attach to the project without being linked to a task -TODO: add a quick button to push to JIRA via default backend. +Change synchronization configuration on a project +""""""""""""""""""""""""""""""""""""""""""""""""" +If you want to change the configuration of a project, such as which +issue types are synchronized, you can open the "Connector" tab in +the project settings and edit the "binding" with the backend. Synchronize tasks and worklogs """""""""""""""""""""""""""""" @@ -114,7 +142,6 @@ imports. It is important to select the issue types so don't miss this step (need Known Issues ------------ -* The Project Jira binding must be saved first then edited again to add the issue types afterwards... * If an odoo user has no linked employee, worklogs will still be imported but with an empty employee * The tasks and worklogs deleted on JIRA are deleted if diff --git a/connector_jira/__init__.py b/connector_jira/__init__.py index 58374e076..4f6372096 100644 --- a/connector_jira/__init__.py +++ b/connector_jira/__init__.py @@ -1,3 +1,4 @@ +from . import cli from . import components from . import controllers from . import models diff --git a/connector_jira/__manifest__.py b/connector_jira/__manifest__.py index 16038e87b..620f7f05e 100644 --- a/connector_jira/__manifest__.py +++ b/connector_jira/__manifest__.py @@ -1,7 +1,7 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) {'name': 'JIRA Connector', - 'version': '11.0.1.0.0', + 'version': '11.0.1.1.0', 'author': 'Camptocamp,Odoo Community Association (OCA)', 'license': 'AGPL-3', 'category': 'Connector', @@ -11,6 +11,7 @@ 'queue_job', 'web', 'web_widget_url_advanced', + 'multi_step_wizard', ], 'external_dependencies': { 'python': [ @@ -23,13 +24,14 @@ # 'requests-toolbelt', # 'PyJWT', 'cryptography', - ], + ], }, 'website': 'https://github.com/camptocamp/connector-jira', 'data': [ 'views/jira_menus.xml', 'wizards/jira_backend_auth_views.xml', 'views/project_link_jira_views.xml', + 'views/task_link_jira_views.xml', 'views/jira_backend_views.xml', 'views/jira_backend_report_templates.xml', 'views/project_project_views.xml', @@ -37,8 +39,12 @@ 'views/res_users_views.xml', 'views/jira_issue_type_views.xml', 'views/timesheet_account_analytic_line.xml', + 'wizards/jira_account_analytic_line_import_views.xml', 'security/ir.model.access.csv', 'data/cron.xml', - ], + ], + 'demo': [ + 'demo/jira_backend_demo.xml', + ], 'installable': True, } diff --git a/connector_jira/cli/__init__.py b/connector_jira/cli/__init__.py new file mode 100644 index 000000000..034f004bd --- /dev/null +++ b/connector_jira/cli/__init__.py @@ -0,0 +1 @@ +from . import jira_oauth_dance diff --git a/connector_jira/cli/jira_oauth_dance.py b/connector_jira/cli/jira_oauth_dance.py new file mode 100644 index 000000000..e686f7ee1 --- /dev/null +++ b/connector_jira/cli/jira_oauth_dance.py @@ -0,0 +1,146 @@ +"""Odoo CLI command to initiate the Oauth access with Jira + +Mostly to be used for the test databases as a wizard does the +same thing from Odoo's UI. + +It is plugged in the Odoo CLI commands. The same way "odoo shell" +can be started from the command line, you can use it with the command:: + + odoo jiraoauthdance + +By default, it will configure the authentication for the Demo Backend +(as it is used in tests). If the demo backend doesn't exist, it will use +on the first Jira backend it can find. You can specify a backend ID with:: + + odoo jiraoauthdance --backend-id=2 + +You have to target the database that you want to link, either in the +configuration file, either using the ``--database`` option. + +""" + +# this is a cli tool, we want to use print statements +# pylint: disable=print-used + +import argparse +import logging +import os +import signal +import sys + +from contextlib import contextmanager + +import odoo +from odoo.cli import Command +from odoo.tools import config + +_logger = logging.getLogger(__name__) + + +def raise_keyboard_interrupt(*a): + raise KeyboardInterrupt() + + +class JiraOauthDance(Command): + + def init(self, args): + config.parse_config(args) + odoo.cli.server.report_configuration() + odoo.service.server.start(preload=[], stop=True) + signal.signal(signal.SIGINT, raise_keyboard_interrupt) + + @contextmanager + def env(self, dbname): + with odoo.api.Environment.manage(): + registry = odoo.registry(dbname) + with registry.cursor() as cr: + uid = odoo.SUPERUSER_ID + ctx_environment = odoo.api.Environment( + cr, uid, {} + )['res.users'] + ctx = ctx_environment.context_get() + env = odoo.api.Environment(cr, uid, ctx) + yield env + + def _find_backend(self, env, backend_id=None): + if backend_id: + backend = env['jira.backend'].browse(backend_id) + if not backend.exists(): + die( + 'no backend with id found {}'.format( + backend_id + ) + ) + else: + backend = env.ref( + 'connector_jira.jira_backend_demo', + raise_if_not_found=False + ) + if not backend: + backend = self.env['jira.backend'].search([], limit=1) + return backend + + def oauth_dance(self, dbname, options): + with self.env(dbname) as env: + backend = self._find_backend(env, backend_id=options.backend_id) + auth_wizard = env['jira.backend.auth'].create({ + 'backend_id': backend.id, + }) + print() + print(r'Welcome to the Jira Oauth dance \o| \o/ |o/') + print() + print( + 'You are working on the backend {} (id: {}) with uri {}' + .format(backend.name, backend.id, backend.uri) + ) + print('Now, copy the consumer and public key ' + 'in the Jira application link') + print() + print('Consumer key:') + print() + print(auth_wizard.consumer_key) + print() + print('Public key:') + print() + print(auth_wizard.public_key) + print() + input('Press any key when you have pasted these values in Jira') + + auth_wizard.do_oauth_leg_1() + print() + print('Jira wants you to open this link (hostname may change if' + ' you use Docker) and approve the link (no clickbait):') + print() + print(auth_wizard.auth_uri) + print() + input('Press any key when approved') + auth_wizard.do_oauth_leg_3() + print() + print('That\'s all folks! Keep these tokens for your tests:') + print() + print('JIRA_TEST_URL="{}"'.format(backend.uri)) + print('JIRA_TEST_TOKEN_ACCESS="{}"'.format(backend.access_token)) + print('JIRA_TEST_TOKEN_SECRET="{}"'.format(backend.access_secret)) + + def run(self, cmdargs): + parser = argparse.ArgumentParser( + prog="%s jiraauthdance" % sys.argv[0].split(os.path.sep)[-1], + description=self.__doc__ + ) + parser.add_argument( + '--backend-id', dest='backend_id', type=int, + help='ID of the backend to authenticate. ' + '(by default the demo backend if exists or the first found)') + + args, unknown = parser.parse_known_args(args=cmdargs) + + self.init(unknown) + if not config['db_name']: + die('need a db_name') + self.oauth_dance(config['db_name'], args) + return 0 + + +def die(message, code=1): + print(message, file=sys.stderr) + sys.exit(code) diff --git a/connector_jira/components/backend_adapter.py b/connector_jira/components/backend_adapter.py index f07a78327..5dbc72a94 100644 --- a/connector_jira/components/backend_adapter.py +++ b/connector_jira/components/backend_adapter.py @@ -1,12 +1,23 @@ -# Copyright 2018 Camptocamp SA +# Copyright 2016-2019 Camptocamp SA # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). import logging +from contextlib import contextmanager + +import requests + +from odoo import _, exceptions from odoo.addons.component.core import Component +from odoo.addons.connector.exception import IDMissingInBackend _logger = logging.getLogger(__name__) +try: + import jira +except ImportError as err: + _logger.debug(err) + JIRA_JQL_DATETIME_FORMAT = '%Y-%m-%d %H:%M' # no seconds :-( @@ -19,4 +30,46 @@ class JiraAdapter(Component): def __init__(self, work_context): super().__init__(work_context) - self.client = self.backend_record.get_api_client() + self._client = None + + @property + def client(self): + # lazy load the client, initialize only when actually needed + if not self._client: + self._client = self.backend_record.get_api_client() + return self._client + + @contextmanager + def handle_404(self): + """Context manager to handle 404 errors on the API + + 404 (no record found) on the API are re-raised as: + ``odoo.addons.connector.exception.IDMissingInBackend`` + """ + try: + yield + except jira.exceptions.JIRAError as err: + if err.status_code == 404: + raise IDMissingInBackend("{} (url: {})".format( + err.text, + err.url, + )) + raise + + @contextmanager + def handle_user_api_errors(self): + """Contextmanager to use when the API is used user-side + + It catches the common network or Jira errors and reraise them + to the user using the Odoo UserError. + """ + try: + yield + except requests.exceptions.ConnectionError as err: + _logger.exception('Jira ConnectionError') + message = _('Error during connection with Jira: %s') % (err,) + raise exceptions.UserError(message) + except jira.exceptions.JIRAError as err: + _logger.exception('Jira JIRAError') + message = _('Jira Error: %s') % (err,) + raise exceptions.UserError(message) diff --git a/connector_jira/components/base.py b/connector_jira/components/base.py index b6e992dba..6d4633a49 100644 --- a/connector_jira/components/base.py +++ b/connector_jira/components/base.py @@ -1,4 +1,4 @@ -# Copyright 2018 Camptocamp SA +# Copyright 2018-2019 Camptocamp SA # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) from odoo.addons.component.core import AbstractComponent diff --git a/connector_jira/components/binder.py b/connector_jira/components/binder.py index f40400339..d33d31c5b 100644 --- a/connector_jira/components/binder.py +++ b/connector_jira/components/binder.py @@ -1,4 +1,4 @@ -# Copyright 2016-2018 Camptocamp SA +# Copyright 2016-2019 Camptocamp SA # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) import logging diff --git a/connector_jira/components/exporter.py b/connector_jira/components/exporter.py index ac0b7f8fb..a85c4d878 100644 --- a/connector_jira/components/exporter.py +++ b/connector_jira/components/exporter.py @@ -1,4 +1,4 @@ -# Copyright 2016-2018 Camptocamp SA +# Copyright 2016-2019 Camptocamp SA # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) import logging @@ -108,7 +108,7 @@ def run(self, binding, *args, **kwargs): # commit so we keep the external ID if several exports # are called and one of them fails if not tools.config['test_enable']: - self.env.cr.commit() + self.env.cr.commit() # pylint: disable=invalid-commit return result def _run(self, *args, **kwargs): @@ -218,7 +218,7 @@ def _export_dependency(self, relation, binding_model, # Eager commit to avoid having 2 jobs # exporting at the same time. if not tools.config['test_enable']: - self.env.cr.commit() + self.env.cr.commit() # pylint: disable=invalid-commit else: # If jira_bind_ids does not exist we are typically in a # "direct" binding (the binding record is the same record). diff --git a/connector_jira/components/importer.py b/connector_jira/components/importer.py index fae8b59fa..afb1d243f 100644 --- a/connector_jira/components/importer.py +++ b/connector_jira/components/importer.py @@ -1,4 +1,4 @@ -# Copyright 2016-2018 Camptocamp SA +# Copyright 2016-2019 Camptocamp SA # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) """ @@ -19,7 +19,7 @@ from psycopg2 import IntegrityError, errorcodes import odoo -from odoo import _ +from odoo import _, tools from odoo.addons.component.core import AbstractComponent, Component from odoo.addons.queue_job.exception import RetryableJobError @@ -52,30 +52,33 @@ def _get_external_data(self): """ Return the raw Jira data for ``self.external_id`` """ return self.backend_adapter.read(self.external_id) - def must_skip(self): - """ Returns a reason if the import should be skipped. - - Returns None to continue with the import + def must_skip(self, force=False): + """Returns a reason as string if the import must be skipped. + Returns None to continue with the import. """ assert self.external_record - return def _before_import(self): """ Hook called before the import, when we have the Jira data""" - def _is_uptodate(self, binding): - """Return True if the import should be skipped because - it is already up-to-date in Odoo""" + def _get_external_updated_at(self): assert self.external_record ext_fields = self.external_record.get('fields', {}) external_updated_at = ext_fields.get('updated') if not external_updated_at: + return None + return iso8601_to_utc_datetime(external_updated_at) + + def _is_uptodate(self, binding): + """Return True if the import should be skipped because + it is already up-to-date in Odoo""" + external_date = self._get_external_updated_at() + if not external_date: return False # no update date on Jira, always import it. if not binding: return # it does not exist so it should not be skipped - external_date = iso8601_to_utc_datetime(external_updated_at) sync_date = self.binder.sync_date(binding) if not sync_date: return @@ -117,7 +120,7 @@ def _import_dependency(self, external_id, binding_model, if component is None: component = self.component(usage='record.importer', model_name=binding_model) - component.run(external_id, record=record) + component.run(external_id, record=record, force=True) def _import_dependencies(self): """ Import the dependencies for the record""" @@ -233,7 +236,21 @@ def do_in_new_work_context(self, model_name=None): cr.rollback() raise else: - cr.commit() + if not tools.config['test_enable']: + cr.commit() # pylint: disable=invalid-commit + + def _handle_record_missing_on_jira(self): + """Hook called when we are importing a record missing on Jira + + By default it deletes the matching record or binding if it exists on + Odoo and returns a result to show on the job, job will be done. + """ + binding = self._get_binding() + if binding: + # emptying the external_id allows to unlink the binding + binding.external_id = False + binding.unlink() + return _('Record does no longer exist in Jira') def run(self, external_id, force=False, record=None, **kwargs): """ Run the synchronization @@ -259,7 +276,7 @@ def run(self, external_id, force=False, record=None, **kwargs): try: self.external_record = self._get_external_data() except IDMissingInBackend: - return _('Record does no longer exist in Jira') + return self._handle_record_missing_on_jira() binding = self._get_binding() if not binding: with self.do_in_new_work_context() as new_work: @@ -310,7 +327,7 @@ def run(self, external_id, force=False, record=None, **kwargs): ignore_retry=True ) - reason = self.must_skip() + reason = self.must_skip(force=force) if reason: return reason diff --git a/connector_jira/components/mapper.py b/connector_jira/components/mapper.py index 3214cc7ca..b39e8cc85 100644 --- a/connector_jira/components/mapper.py +++ b/connector_jira/components/mapper.py @@ -1,4 +1,4 @@ -# Copyright 2016-2018 Camptocamp SA +# Copyright 2016-2019 Camptocamp SA # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) import pytz diff --git a/connector_jira/controllers/main.py b/connector_jira/controllers/main.py index 81bed2c2f..78a763be2 100644 --- a/connector_jira/controllers/main.py +++ b/connector_jira/controllers/main.py @@ -1,4 +1,4 @@ -# Copyright 2016 Camptocamp SA +# Copyright 2016-2019 Camptocamp SA # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) """ diff --git a/connector_jira/demo/jira_backend_demo.xml b/connector_jira/demo/jira_backend_demo.xml new file mode 100644 index 000000000..abba2741a --- /dev/null +++ b/connector_jira/demo/jira_backend_demo.xml @@ -0,0 +1,9 @@ + + + + + Jira + http://jira:8080 + + + diff --git a/connector_jira/migrations/11.0.1.1.0/pre-migration.py b/connector_jira/migrations/11.0.1.1.0/pre-migration.py new file mode 100644 index 000000000..ce9a38ca5 --- /dev/null +++ b/connector_jira/migrations/11.0.1.1.0/pre-migration.py @@ -0,0 +1,41 @@ +# Copyright 2019 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) + + +def migrate(cr, version): + if not version: + return + cr.execute(""" + ALTER TABLE jira_project_project + DROP CONSTRAINT IF EXISTS + jira_project_project_jira_binding_backend_uniq; + """) + + # copy the jira_key from project to binding before we change it + # as a computed field + cr.execute(""" + ALTER TABLE jira_project_project + ADD COLUMN jira_key VARCHAR(10); + """) + cr.execute(""" + UPDATE jira_project_project + SET jira_key = project_project.jira_key + FROM project_project + WHERE project_project.id = jira_project_project.odoo_id; + """) + cr.execute(""" + ALTER TABLE jira_project_project + DROP COLUMN jira_key; + """) + + cr.execute(""" + ALTER TABLE jira_project_project + ADD COLUMN project_type VARCHAR; + """) + # we don't know the correct value, set software by default + # until 11.0.1.1.0 we cannot have more than one binding, + # so the constraint will not fail anyway + cr.execute(""" + UPDATE jira_project_project + SET project_type = 'software'; + """) diff --git a/connector_jira/models/__init__.py b/connector_jira/models/__init__.py index e231f565d..9f9812d67 100644 --- a/connector_jira/models/__init__.py +++ b/connector_jira/models/__init__.py @@ -6,3 +6,4 @@ from . import project_project from . import project_task from . import res_users +from . import queue_job diff --git a/connector_jira/models/account_analytic_line/common.py b/connector_jira/models/account_analytic_line/common.py index da79cdcd8..7c5ece021 100644 --- a/connector_jira/models/account_analytic_line/common.py +++ b/connector_jira/models/account_analytic_line/common.py @@ -1,8 +1,8 @@ -# Copyright 2016 Camptocamp SA +# Copyright 2016-2019 Camptocamp SA # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) from odoo import api, fields, models -from odoo.addons.queue_job.job import job +from odoo.addons.queue_job.job import job, related_action from odoo.addons.component.core import Component @@ -22,18 +22,57 @@ class JiraAccountAnalyticLine(models.Model): # in case we'll need it for an eventual export jira_issue_id = fields.Char() + # we have to store these fields on the analytic line because + # they may be different than the ones on their odoo task: + # for instance, we do not import "Tasks" but we import "Epics", + # the analytic line for a "Task" will be linked to an "Epic" on + # Odoo, but we still want to know the original task here + jira_issue_key = fields.Char( + string='Original Task Key', + readonly=True, + ) + jira_issue_type_id = fields.Many2one( + comodel_name='jira.issue.type', + string='Original Issue Type', + readonly=True, + ) + jira_issue_url = fields.Char( + string='Original JIRA issue Link', + compute='_compute_jira_issue_url', + ) + jira_epic_issue_key = fields.Char( + string='Original Epic Key', + readonly=True, + ) + jira_epic_issue_url = fields.Char( + string='Original JIRA Epic Link', + compute='_compute_jira_issue_url', + ) + _sql_constraints = [ ('jira_binding_backend_uniq', 'unique(backend_id, odoo_id)', "A binding already exists for this line and this backend."), ] + @api.depends('jira_issue_key', 'jira_epic_issue_key') + def _compute_jira_issue_url(self): + """Compute the external URL to JIRA.""" + for record in self: + record.jira_issue_url = self.backend_id.make_issue_url( + record.jira_issue_key + ) + record.jira_epic_issue_url = self.backend_id.make_issue_url( + record.jira_epic_issue_key + ) + @job(default_channel='root.connector_jira.import') + @related_action(action="related_action_jira_link") @api.model def import_record(self, backend, issue_id, worklog_id, force=False): """ Import a worklog from JIRA """ with backend.work_on(self._name) as work: importer = work.component(usage='record.importer') - importer.run(worklog_id, issue_id=issue_id, force=force) + return importer.run(worklog_id, issue_id=issue_id, force=force) @job(default_channel='root.connector_jira.import') @api.model @@ -41,7 +80,17 @@ def delete_record(self, backend, issue_id, worklog_id): """ Delete a local worklog which has been deleted on JIRA """ with backend.work_on(self._name) as work: importer = work.component(usage='record.deleter') - importer.run(worklog_id) + return importer.run(worklog_id) + + @api.multi + def force_reimport(self): + for binding in self.mapped('jira_bind_ids'): + binding.with_delay(priority=8).import_record( + binding.backend_id, + binding.jira_issue_id, + binding.external_id, + force=True, + ) class AccountAnalyticLine(models.Model): @@ -55,34 +104,57 @@ class AccountAnalyticLine(models.Model): context={'active_test': False}, ) # fields needed to display JIRA issue link in views - jira_compound_key = fields.Char( - related='task_id.jira_compound_key', + jira_issue_key = fields.Char( + string='Original JIRA Issue Key', + compute='_compute_jira_references', readonly=True, store=True, ) jira_issue_url = fields.Char( - related='task_id.jira_issue_url', + string='Original JIRA issue Link', + compute='_compute_jira_references', readonly=True, ) - - jira_epic_compound_key = fields.Char( - related='task_id.jira_epic_link_task_id.jira_compound_key', + jira_epic_issue_key = fields.Char( + compute='_compute_jira_references', + string='Original JIRA Epic Key', readonly=True, - store=True + store=True, ) - jira_epic_issue_url = fields.Char( - string='JIRA epic URL', - related='task_id.jira_epic_link_task_id.jira_issue_url', + string='Original JIRA Epic Link', + compute='_compute_jira_references', readonly=True ) - jira_issue_type = fields.Char( - related='task_id.jira_issue_type', + jira_issue_type_id = fields.Many2one( + comodel_name='jira.issue.type', + string='Original JIRA Issue Type', + compute='_compute_jira_references', readonly=True, store=True ) + @api.depends( + 'jira_bind_ids.jira_issue_key', + 'jira_bind_ids.jira_issue_type_id', + 'jira_bind_ids.jira_epic_issue_key', + ) + def _compute_jira_references(self): + """Compute the various references to JIRA. + + We assume that we have only one external record for a line + """ + for record in self: + if not record.jira_bind_ids: + continue + main_binding = record.jira_bind_ids[0] + record.jira_issue_key = main_binding.jira_issue_key + record.jira_issue_url = main_binding.jira_issue_url + record.jira_issue_type_id = main_binding.jira_issue_type_id + record.jira_epic_issue_key = main_binding.jira_epic_issue_key + record.jira_epic_issue_url = main_binding.jira_epic_issue_url + class WorklogAdapter(Component): _name = 'jira.worklog.adapter' @@ -90,7 +162,8 @@ class WorklogAdapter(Component): _apply_on = ['jira.account.analytic.line'] def read(self, issue_id, worklog_id): - return self.client.worklog(issue_id, worklog_id).raw + with self.handle_404(): + return self.client.worklog(issue_id, worklog_id).raw def search(self, issue_id): """ Search worklogs of an issue """ diff --git a/connector_jira/models/account_analytic_line/importer.py b/connector_jira/models/account_analytic_line/importer.py index ba28a82fb..f4d1cacb0 100644 --- a/connector_jira/models/account_analytic_line/importer.py +++ b/connector_jira/models/account_analytic_line/importer.py @@ -8,7 +8,9 @@ from odoo.addons.connector.components.mapper import mapping, only_create from odoo.addons.component.core import Component from ...components.backend_adapter import JIRA_JQL_DATETIME_FORMAT -from ...components.mapper import iso8601_local_date, whenempty +from ...components.mapper import ( + iso8601_local_date, iso8601_to_utc_datetime, whenempty +) _logger = logging.getLogger(__name__) @@ -30,7 +32,21 @@ def default(self, record): @mapping def issue(self, record): - return {'jira_issue_id': record['issueId']} + issue = self.options.linked_issue + assert issue + refs = { + 'jira_issue_id': record['issueId'], + 'jira_issue_key': issue['key'], + } + task_mapper = self.component( + usage='import.mapper', + model_name='jira.project.task', + ) + refs.update(task_mapper.issue_type(issue)) + epic_field_name = self.backend_record.epic_link_field_name + if epic_field_name: + refs['jira_epic_issue_key'] = issue['fields'][epic_field_name] + return refs @mapping def duration(self, record): @@ -59,11 +75,18 @@ def author(self, record): @mapping def project_and_task(self, record): - assert self.options.task_binding or self.options.project_binding + assert ( + self.options.task_binding or + self.options.project_binding or + self.options.fallback_project + ) task_binding = self.options.task_binding if not task_binding: project = self.options.project_binding.odoo_id - return {'project_id': project.id} + if project: + return {'project_id': project.id} + else: + return {'project_id': self.options.fallback_project.id} project = task_binding.project_id return {'task_id': task_binding.odoo_id.id, @@ -119,6 +142,14 @@ def __init__(self, work_context): self.external_issue_id = None self.task_binding = None self.project_binding = None + self.fallback_project = None + + def _get_external_updated_at(self): + assert self.external_record + external_updated_at = self.external_record.get('updated') + if not external_updated_at: + return None + return iso8601_to_utc_datetime(external_updated_at) @property def _issue_fields_to_read(self): @@ -184,12 +215,14 @@ def _create_data(self, map_record, **kwargs): return super()._create_data(map_record, task_binding=self.task_binding, project_binding=self.project_binding, + fallback_project=self.fallback_project, linked_issue=self.external_issue) def _update_data(self, map_record, **kwargs): return super()._update_data(map_record, task_binding=self.task_binding, project_binding=self.project_binding, + fallback_project=self.fallback_project, linked_issue=self.external_issue) def run(self, external_id, force=False, record=None, **kwargs): @@ -199,6 +232,19 @@ def run(self, external_id, force=False, record=None, **kwargs): external_id, force=force, record=record, **kwargs ) + def _handle_record_missing_on_jira(self): + """Hook called when we are importing a record missing on Jira + + For worklogs, we drop the analytic line if we discover it doesn't exist + on Jira, as the latter is the master. + """ + binding = self._get_binding() + if binding: + record = binding.odoo_id + binding.unlink() + record.unlink() + return _('Record does no longer exist in Jira') + def _get_external_data(self): """ Return the raw Jira data for ``self.external_id`` """ issue_adapter = self.component( @@ -219,9 +265,13 @@ def _before_import(self): assert issue matcher = self.component(usage='jira.task.project.matcher') self.project_binding = matcher.find_project_binding(issue) + if not self.project_binding: + self.fallback_project = matcher.fallback_project_for_worklogs() def _import(self, binding, **kwargs): - if not self.task_binding and not self.project_binding: + if not (self.task_binding or + self.project_binding or + self.fallback_project): _logger.debug( "No task or project synchronized for attaching worklog %s", self.external_record['id'] @@ -229,10 +279,13 @@ def _import(self, binding, **kwargs): return return super()._import(binding, **kwargs) - def _import_dependencies(self): - """ Import the dependencies for the record""" + def _import_dependency_assignee(self): jira_assignee = self.external_record['author'] jira_key = jira_assignee.get('key') self._import_dependency(jira_key, 'jira.res.users', record=jira_assignee) + + def _import_dependencies(self): + """ Import the dependencies for the record""" + self._import_dependency_assignee() diff --git a/connector_jira/models/jira_backend/common.py b/connector_jira/models/jira_backend/common.py index 7fc36c96f..31d27ea9d 100644 --- a/connector_jira/models/jira_backend/common.py +++ b/connector_jira/models/jira_backend/common.py @@ -1,5 +1,5 @@ # Copyright: 2015 LasLabs, Inc. -# Copyright 2016 Camptocamp SA +# Copyright 2016-2019 Camptocamp SA # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) import binascii @@ -12,21 +12,31 @@ from os import urandom import psycopg2 -from cryptography.hazmat.backends import default_backend -from cryptography.hazmat.primitives import serialization -from cryptography.hazmat.primitives.asymmetric import rsa -from jira import JIRA, JIRAError -from jira.utils import json_loads +import requests import odoo -from odoo import models, fields, api, exceptions, _ +from odoo import models, fields, api, exceptions, _, tools from odoo.addons.component.core import Component _logger = logging.getLogger(__name__) +JIRA_TIMEOUT = 30 # seconds IMPORT_DELTA = 70 # seconds +try: + from jira import JIRA, JIRAError + from jira.utils import json_loads +except ImportError: + pass # already logged in components/backend_adapter.py + +try: + from cryptography.hazmat.backends import default_backend + from cryptography.hazmat.primitives import serialization + from cryptography.hazmat.primitives.asymmetric import rsa +except ImportError as err: + _logger.debug(err) + @contextmanager def new_env(env): @@ -40,7 +50,8 @@ def new_env(env): cr.rollback() raise else: - cr.commit() + if not tools.config['test_enable']: + cr.commit() # pylint: disable=invalid-commit class JiraBackend(models.Model): @@ -67,6 +78,15 @@ def _default_consumer_key(self): required=True, default=lambda self: self._default_company(), ) + worklog_fallback_project_id = fields.Many2one( + comodel_name="project.project", + string="Fallback for Worklogs", + help="Worklogs which could not be linked to any project " + "will be created in this project. Worklogs landing in " + "the fallback project can be reassigned to the correct " + "project by: 1. linking the expected project with the Jira one, " + "2. using 'Refresh Worklogs from Jira' on the timesheet lines." + ) state = fields.Selection( selection=[('authenticate', 'Authenticate'), ('setup', 'Setup'), @@ -140,6 +160,11 @@ def _default_consumer_key(self): help="The 'Epic Link' field on JIRA is a custom field. " "The name of the field is something like 'customfield_10002'. " ) + epic_name_field_name = fields.Char( + string='Epic Name Field', + help="The 'Epic Name' field on JIRA is a custom field. " + "The name of the field is something like 'customfield_10003'. " + ) odoo_webhook_base_url = fields.Char( string='Base Odoo URL for Webhooks', @@ -353,7 +378,8 @@ def activate_epic_link(self): custom_ref = field.get('schema', {}).get('custom') if custom_ref == 'com.pyxis.greenhopper.jira:gh-epic-link': self.epic_link_field_name = field['id'] - break + elif custom_ref == 'com.pyxis.greenhopper.jira:gh-epic-label': + self.epic_name_field_name = field['id'] @api.multi def state_setup(self): @@ -413,7 +439,8 @@ def create_webhooks(self): # u'http://jira:8080/rest/webhooks/1.0/webhook/5' webhook_id = webhook['self'].split('/')[-1] backend.webhook_issue_jira_id = webhook_id - env.cr.commit() + if not tools.config['test_enable']: + env.cr.commit() # pylint: disable=invalid-commit url = urllib.parse.urljoin(base_url, '/connector_jira/webhooks/worklog') @@ -427,7 +454,8 @@ def create_webhooks(self): ) webhook_id = webhook['self'].split('/')[-1] backend.webhook_worklog_jira_id = webhook_id - env.cr.commit() + if not tools.config['test_enable']: + env.cr.commit() # pylint: disable=invalid-commit @api.onchange('odoo_webhook_base_url') def onchange_odoo_webhook_base_url(self): @@ -461,8 +489,8 @@ def delete_webhooks(self): def check_connection(self): self.ensure_one() try: - self.get_api_client() - except ValueError as err: + self.get_api_client().myself() + except (ValueError, requests.exceptions.ConnectionError) as err: raise exceptions.UserError( _('Failed to connect (%s)') % (err,) ) @@ -505,17 +533,20 @@ def import_issue_type(self): @api.model def get_api_client(self): + self.ensure_one() + # tokens are only readable by connector managers + backend = self.sudo() oauth = { - 'access_token': self.access_token, - 'access_token_secret': self.access_secret, - 'consumer_key': self.consumer_key, - 'key_cert': self.private_key, + 'access_token': backend.access_token, + 'access_token_secret': backend.access_secret, + 'consumer_key': backend.consumer_key, + 'key_cert': backend.private_key, } options = { - 'server': self.uri, - 'verify': self.verify_ssl, + 'server': backend.uri, + 'verify': backend.verify_ssl, } - return JIRA(options=options, oauth=oauth) + return JIRA(options=options, oauth=oauth, timeout=JIRA_TIMEOUT) @api.model def _scheduler_import_project_task(self): diff --git a/connector_jira/models/jira_binding/common.py b/connector_jira/models/jira_binding/common.py index 23fdfbcef..710768320 100644 --- a/connector_jira/models/jira_binding/common.py +++ b/connector_jira/models/jira_binding/common.py @@ -1,4 +1,4 @@ -# Copyright 2016 Camptocamp SA +# Copyright 2016-2019 Camptocamp SA # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) from odoo import api, fields, models @@ -36,15 +36,16 @@ def import_batch(self, backend, from_date=None, to_date=None): """ Prepare import of a batch of records """ with backend.work_on(self._name) as work: importer = work.component(usage='batch.importer') - importer.run(from_date=from_date, to_date=to_date) + return importer.run(from_date=from_date, to_date=to_date) @job(default_channel='root.connector_jira.import') + @related_action(action="related_action_jira_link") @api.model def import_record(self, backend, external_id, force=False): """ Import a record """ with backend.work_on(self._name) as work: importer = work.component(usage='record.importer') - importer.run(external_id, force=force) + return importer.run(external_id, force=force) @job(default_channel='root.connector_jira.import') @api.model @@ -52,7 +53,7 @@ def delete_record(self, backend, external_id): """ Delete a record on Odoo """ with backend.work_on(self._name) as work: importer = work.component(usage='record.deleter') - importer.run(external_id) + return importer.run(external_id) @job(default_channel='root.connector_jira.export') @related_action(action='related_action_unwrap_binding') @@ -61,4 +62,4 @@ def export_record(self, fields=None): self.ensure_one() with self.backend_id.work_on(self._name) as work: exporter = work.component(usage='record.exporter') - exporter.run(self, fields=fields) + return exporter.run(self, fields=fields) diff --git a/connector_jira/models/jira_issue_type/common.py b/connector_jira/models/jira_issue_type/common.py index 9acaf34cc..943298a7c 100644 --- a/connector_jira/models/jira_issue_type/common.py +++ b/connector_jira/models/jira_issue_type/common.py @@ -1,4 +1,4 @@ -# Copyright 2016 Camptocamp SA +# Copyright 2016-2019 Camptocamp SA # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) from odoo import api, fields, models @@ -42,7 +42,8 @@ class IssueTypeAdapter(Component): _apply_on = ['jira.issue.type'] def read(self, id_): - return self.client.issue_type(id_).raw + with self.handle_404(): + return self.client.issue_type(id_).raw def search(self): issues = self.client.issue_types() diff --git a/connector_jira/models/jira_issue_type/importer.py b/connector_jira/models/jira_issue_type/importer.py index f8a47aeea..2e5666ef7 100644 --- a/connector_jira/models/jira_issue_type/importer.py +++ b/connector_jira/models/jira_issue_type/importer.py @@ -1,4 +1,4 @@ -# Copyright 2016 Camptocamp SA +# Copyright 2016-2019 Camptocamp SA # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) from odoo.addons.connector.components.mapper import mapping diff --git a/connector_jira/models/project_project/__init__.py b/connector_jira/models/project_project/__init__.py index a7c605293..b21d0ef6f 100644 --- a/connector_jira/models/project_project/__init__.py +++ b/connector_jira/models/project_project/__init__.py @@ -1,3 +1,4 @@ +from . import binder from . import common from . import project_link_jira from . import exporter diff --git a/connector_jira/models/project_project/binder.py b/connector_jira/models/project_project/binder.py new file mode 100644 index 000000000..7b56dc3a8 --- /dev/null +++ b/connector_jira/models/project_project/binder.py @@ -0,0 +1,50 @@ +# Copyright 2016-2019 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) + +import logging + +from odoo import models +from odoo.addons.component.core import Component + +_logger = logging.getLogger(__name__) + + +class JiraProjectBinder(Component): + _name = 'jira.project.binder' + _inherit = 'jira.binder' + + _apply_on = [ + 'jira.project.project', + ] + + def to_external(self, binding, wrap=False, project_type=None): + """ Give the external ID for an Odoo binding ID + + More than one jira binding is tolerated on projects, but we can have + only one binding for each type of project (software, service_desk, + business, ...). + + :param binding: Odoo binding for which we want the external id + :param wrap: if True, binding is a normal record, the + method will search the corresponding binding and return + the external id of the binding + :return: external ID of the record + """ + if not project_type: + raise ValueError('project_type argument is required') + if isinstance(binding, models.BaseModel): + binding.ensure_one() + else: + binding = self.model.browse(binding) + if wrap: + binding = self.model.with_context(active_test=False).search( + [(self._odoo_field, '=', binding.id), + (self._backend_field, '=', self.backend_record.id), + ('project_type', '=', project_type), + ] + ) + if not binding: + return None + binding.ensure_one() + return binding[self._external_field] + return binding[self._external_field] diff --git a/connector_jira/models/project_project/common.py b/connector_jira/models/project_project/common.py index 73b79db3b..94ee78fa9 100644 --- a/connector_jira/models/project_project/common.py +++ b/connector_jira/models/project_project/common.py @@ -1,4 +1,4 @@ -# Copyright 2016 Camptocamp SA +# Copyright 2016-2019 Camptocamp SA # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) import json @@ -6,8 +6,11 @@ import re import tempfile -from jira import JIRAError -from jira.utils import json_loads +try: + from jira import JIRAError + from jira.utils import json_loads +except ImportError: + pass # already logged in components/adapter.py from odoo import api, fields, models, exceptions, _, tools @@ -24,6 +27,11 @@ class JiraProjectBaseFields(models.AbstractModel): """ _name = 'jira.project.base.mixin' + jira_key = fields.Char( + string='JIRA Key', + kequired=True, + size=10, # limit on JIRA + ) sync_issue_type_ids = fields.Many2many( comodel_name='jira.issue.type', string='Issue Levels to Synchronize', @@ -72,11 +80,16 @@ class JiraProjectProject(models.Model): required=True, index=True, ondelete='restrict') + project_type = fields.Selection( + selection="_selection_project_type" + ) - _sql_constraints = [ - ('jira_binding_backend_uniq', 'unique(backend_id, odoo_id)', - "A binding already exists for this project and this backend."), - ] + @api.model + def _selection_project_type(self): + return [ + ('software', 'Software'), + ('business', 'Business'), + ] # Disable and implement the constraint jira_binding_uniq as python because # we need to override the in connector_jira_service_desk and it would try @@ -100,13 +113,66 @@ def _add_sql_constraints(self): self._sql_constraints = constraints super()._add_sql_constraints() + def _other_same_type_domain(self): + """Return the domain to search a binding on the same project and type + + It is used for the constraint allowing only one binding of each type. + The reason for this is: + + * supporting several projects of different types is a requirements (eg. + 1 service desk and 1 software) + * but if we implement new features like "if I create a task it is + pushed to Jira", with different projects we would not know where to + push them + + Using this constraint, we'll be able to focus new export features by + project type. + + """ + self.ensure_one() + domain = [ + ('odoo_id', '=', self.odoo_id.id), + ('backend_id', '=', self.backend_id.id), + ('project_type', '=', self.project_type) + ] + if self.id: + domain.append( + ('id', '!=', self.id), + ) + return domain + + @api.constrains('backend_id', 'odoo_id', 'project_type') + def _constrains_odoo_jira_uniq(self): + """Add a constraint on backend+odoo id + + More than one binding is tolerated but only one can be a master + binding. The master binding will be used when we have to push data from + Odoo to Jira (add tasks, ...). + """ + for binding in self: + same_link_bindings = self.with_context(active_test=False).search( + self._other_same_type_domain() + ) + if same_link_bindings: + raise exceptions.ValidationError(_( + "The project \"%s\" already has a binding with " + "a Jira project of the same type (%s)." + ) % (binding.display_name, self.project_type)) + @api.constrains('backend_id', 'external_id') def _constrains_jira_uniq(self): + """Add a constraint on backend+jira id + + Defined as a python method rather than a postgres constraint + in order to ease the override in connector_jira_servicedesk + """ for binding in self: - same_link_bindings = self.search([ - ('id', '!=', self.id), - ('backend_id', '=', self.backend_id.id), - ('external_id', '=', self.external_id), + if not binding.external_id: + continue + same_link_bindings = self.with_context(active_test=False).search([ + ('id', '!=', binding.id), + ('backend_id', '=', binding.backend_id.id), + ('external_id', '=', binding.external_id), ]) if same_link_bindings: raise exceptions.ValidationError(_( @@ -114,6 +180,16 @@ def _constrains_jira_uniq(self): " JIRA project." ) % (same_link_bindings.display_name)) + @api.constrains('jira_key') + def check_jira_key(self): + for project in self: + if not project.jira_key: + continue + if not self._jira_key_valid(project.jira_key): + raise exceptions.ValidationError( + _('%s is not a valid JIRA Key') % project.jira_key + ) + @api.onchange('backend_id') def onchange_project_backend_id(self): self.project_template = self.backend_id.project_template @@ -157,7 +233,7 @@ def _ensure_jira_key(self): for record in self: if not record.jira_key: raise exceptions.UserError( - _('The JIRA Key is mandatory in order to export a project') + _('The JIRA Key is mandatory in order to link a project') ) @api.multi @@ -179,40 +255,16 @@ class ProjectProject(models.Model): string='Project Bindings', context={'active_test': False}, ) - jira_exportable = fields.Boolean( - string='Exportable on Jira', - compute='_compute_jira_exportable', - ) jira_key = fields.Char( string='JIRA Key', - size=10, # limit on JIRA + compute='_compute_jira_key', ) - @api.constrains('jira_key') - def check_jira_key(self): - for project in self: - if not project.jira_key: - continue - valid = self.env['jira.project.project']._jira_key_valid - if not valid(project.jira_key): - raise exceptions.ValidationError( - _('%s is not a valid JIRA Key') % project.jira_key - ) - - @api.depends('jira_bind_ids') - def _compute_jira_exportable(self): + @api.depends('jira_bind_ids.jira_key') + def _compute_jira_key(self): for project in self: - project.jira_exportable = bool(project.jira_bind_ids) - - @api.multi - def write(self, values): - result = super().write(values) - for record in self: - if record.jira_exportable and not record.jira_key: - raise exceptions.UserError( - _('The JIRA Key is mandatory on JIRA projects.') - ) - return result + keys = project.mapped('jira_bind_ids.jira_key') + project.jira_key = ', '.join(keys) @api.multi def name_get(self): @@ -243,13 +295,16 @@ class ProjectAdapter(Component): _apply_on = ['jira.project.project'] def read(self, id_): - return self.get(id_).raw + with self.handle_404(): + return self.get(id_).raw def get(self, id_): - return self.client.project(id_) + with self.handle_404(): + return self.client.project(id_) def write(self, id_, values): - self.get(id_).update(values) + with self.handle_404(): + self.get(id_).update(values) def create(self, key=None, name=None, template_name=None, values=None): project = self.client.create_project( diff --git a/connector_jira/models/project_project/exporter.py b/connector_jira/models/project_project/exporter.py index 795d294a0..6bbedbf60 100644 --- a/connector_jira/models/project_project/exporter.py +++ b/connector_jira/models/project_project/exporter.py @@ -1,4 +1,4 @@ -# Copyright 2016 Camptocamp SA +# Copyright 2016-2019 Camptocamp SA # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) from odoo.addons.component.core import Component diff --git a/connector_jira/models/project_project/project_link_jira.py b/connector_jira/models/project_project/project_link_jira.py index 7fd72f395..462ad439c 100644 --- a/connector_jira/models/project_project/project_link_jira.py +++ b/connector_jira/models/project_project/project_link_jira.py @@ -1,10 +1,8 @@ -# Copyright 2018 Camptocamp SA +# Copyright 2019 Camptocamp SA # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) import logging -import jira - from odoo import api, fields, models, exceptions, _ _logger = logging.getLogger(__name__) @@ -12,46 +10,31 @@ class ProjectLinkJira(models.TransientModel): _name = 'project.link.jira' - _inherit = 'jira.project.base.mixin' + _inherit = ['jira.project.base.mixin', 'multi.step.wizard.mixin'] _description = 'Link Project with JIRA' project_id = fields.Many2one( comodel_name='project.project', name="Project", required=True, + ondelete='cascade', default=lambda self: self._default_project_id(), ) jira_key = fields.Char( - string='JIRA Key', - size=10, # limit on JIRA - required=True, default=lambda self: self._default_jira_key(), ) backend_id = fields.Many2one( comodel_name='jira.backend', string='Jira Backend', required=True, - ondelete='restrict', + ondelete='cascade', default=lambda self: self._default_backend_id(), ) - state = fields.Selection( - selection='_selection_state', - default='start', - required=True, - ) jira_project_id = fields.Many2one( comodel_name='jira.project.project', + ondelete='cascade', ) - @api.model - def _selection_state(self): - return [ - ('start', 'Start'), - ('issue_types', 'Issue Types'), - ('export_config', 'Export Config.'), - ('final', 'Final'), - ] - @api.model def _default_project_id(self): return self.env.context.get('active_id') @@ -62,8 +45,6 @@ def _default_jira_key(self): if not project_id: return project = self.env['project.project'].browse(project_id) - if project.jira_key: - return project.jira_key valid = self.env['jira.project.project']._jira_key_valid if valid(project.name): return project.name @@ -74,6 +55,15 @@ def _default_backend_id(self): if len(backends) == 1: return backends.id + @api.model + def _selection_state(self): + return [ + ('start', 'Start'), + ('issue_types', 'Issue Types'), + ('export_config', 'Export Config.'), + ('final', 'Final'), + ] + @api.constrains('jira_key') def check_jira_key(self): for record in self: @@ -89,20 +79,6 @@ def add_all_issue_types(self): ]) self.sync_issue_type_ids = issue_types.ids - def open_next(self): - state_method = getattr(self, 'state_exit_%s' % (self.state)) - state_method() - return self._reopen_self() - - def _reopen_self(self): - return { - 'type': 'ir.actions.act_window', - 'res_model': self._name, - 'res_id': self.id, - 'view_mode': 'form', - 'target': 'new', - } - def state_exit_start(self): if self.sync_action == 'export': self.add_all_issue_types() @@ -123,16 +99,24 @@ def state_exit_export_config(self): self._create_export_binding() self.state = 'final' - def _prepare_export_binding_values(self): + def _prepare_base_binding_values(self): values = { 'backend_id': self.backend_id.id, 'odoo_id': self.project_id.id, 'jira_key': self.jira_key, + } + return values + + def _prepare_export_binding_values(self): + values = self._prepare_base_binding_values() + values.update({ + 'backend_id': self.backend_id.id, + 'odoo_id': self.project_id.id, 'sync_action': 'export', 'sync_issue_type_ids': [(6, 0, self.sync_issue_type_ids.ids)], 'project_template': self.project_template, 'project_template_shared': self.project_template_shared, - } + }) return values def _create_export_binding(self): @@ -142,15 +126,8 @@ def _create_export_binding(self): def _link_binding(self): with self.backend_id.work_on('jira.project.project') as work: adapter = work.component(usage='backend.adapter') - try: + with adapter.handle_user_api_errors(): jira_project = adapter.get(self.jira_key) - except jira.exceptions.JIRAError: - _logger.exception('Error when linking to project %s', - self.project_id.id) - raise exceptions.UserError( - _('Could not link %s, check that this project' - ' keys exists in JIRA.') % (self.jira_key) - ) self._link_with_jira_project(work, jira_project) def _link_with_jira_project(self, work, jira_project): @@ -168,13 +145,12 @@ def _link_with_jira_project(self, work, jira_project): self.sync_issue_type_ids = issue_types.ids def _prepare_link_binding_values(self, jira_project): - values = { - 'backend_id': self.backend_id.id, - 'odoo_id': self.project_id.id, - 'jira_key': self.jira_key, + values = self._prepare_base_binding_values() + values.update({ 'sync_action': self.sync_action, 'external_id': jira_project.id, - } + 'project_type': jira_project.projectTypeKey, + }) return values def _copy_issue_types(self): diff --git a/connector_jira/models/project_task/__init__.py b/connector_jira/models/project_task/__init__.py index 79ab5dc6b..87f746c0f 100644 --- a/connector_jira/models/project_task/__init__.py +++ b/connector_jira/models/project_task/__init__.py @@ -1,2 +1,3 @@ from . import common from . import importer +from . import task_link_jira diff --git a/connector_jira/models/project_task/common.py b/connector_jira/models/project_task/common.py index c1282495c..f4b813078 100644 --- a/connector_jira/models/project_task/common.py +++ b/connector_jira/models/project_task/common.py @@ -1,4 +1,4 @@ -# Copyright 2016-2018 Camptocamp SA +# Copyright 2016-2019 Camptocamp SA # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) from odoo import api, fields, models, exceptions, _ @@ -38,6 +38,10 @@ class JiraProjectTask(models.Model): "Empty if the type of parent is filtered out " "of the synchronizations.", ) + jira_issue_url = fields.Char( + string='JIRA issue', + compute='_compute_jira_issue_url', + ) _sql_constraints = [ ('jira_binding_backend_uniq', 'unique(backend_id, odoo_id)', @@ -52,6 +56,14 @@ def unlink(self): ) return super().unlink() + @api.depends('jira_key') + def _compute_jira_issue_url(self): + """Compute the external URL to JIRA.""" + for record in self: + record.jira_issue_url = record.backend_id.make_issue_url( + record.jira_key + ) + class ProjectTask(models.Model): _inherit = 'project.task' @@ -130,9 +142,7 @@ def _compute_jira_issue_url(self): if not record.jira_bind_ids: continue main_binding = record.jira_bind_ids[0] - record.jira_issue_url = main_binding.backend_id.make_issue_url( - main_binding.jira_key - ) + record.jira_issue_url = main_binding.jira_issue_url @api.multi def name_get(self): @@ -151,7 +161,12 @@ class TaskAdapter(Component): _apply_on = ['jira.project.task'] def read(self, id_, fields=None): - return self.client.issue(id_, fields=fields).raw + with self.handle_404(): + return self.client.issue(id_, fields=fields).raw + + def get(self, id_): + with self.handle_404(): + return self.client.issue(id_) def search(self, jql): # we need to have at least one field which is not 'id' or 'key' diff --git a/connector_jira/models/project_task/importer.py b/connector_jira/models/project_task/importer.py index c309ea32d..bdec57de0 100644 --- a/connector_jira/models/project_task/importer.py +++ b/connector_jira/models/project_task/importer.py @@ -1,4 +1,4 @@ -# Copyright 2016 Camptocamp SA +# Copyright 2016-2019 Camptocamp SA # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) from odoo import _ @@ -17,7 +17,6 @@ class ProjectTaskMapper(Component): ] from_fields = [ - ('summary', 'name'), ('duedate', 'date_deadline'), ] @@ -27,6 +26,24 @@ def from_attributes(self, record): record, self ) + @mapping + def name(self, record): + # On an Epic, you have 2 fields: + + # a field like 'customfield_10003' labelled "Epic Name" + # a field 'summary' labelled "Sumarry" + + # The other types of tasks have only the 'summary' field, the other is + # empty. To simplify, we always try to read the Epic Name, which + # will always be empty for other types. + epic_name_field = self.backend_record.epic_name_field_name + name = False + if epic_name_field: + name = record['fields'].get(epic_name_field) + if not name: + name = record['fields']['summary'] + return {'name': name} + @mapping def issue_type(self, record): binder = self.binder_for('jira.issue.type') @@ -86,6 +103,16 @@ def parent(self, record): def backend_id(self, record): return {'backend_id': self.backend_record.id} + def finalize(self, map_record, values): + values = values.copy() + if values.get('odoo_id'): + # If a mapping binds the issue to an existing odoo + # task, we should not change the project. + # It's not only unexpected, but would fail as soon + # as we have invoiced timesheet lines on the task. + values.pop('project_id') + return values + class ProjectTaskBatchImporter(Component): """ Import the Jira tasks @@ -104,10 +131,13 @@ class ProjectTaskProjectMatcher(Component): _usage = 'jira.task.project.matcher' def find_project_binding(self, jira_task_data, unwrap=False): - jira_project_id = self.external_record['fields']['project']['id'] + jira_project_id = jira_task_data['fields']['project']['id'] binder = self.binder_for('jira.project.project') return binder.to_internal(jira_project_id, unwrap=unwrap) + def fallback_project_for_worklogs(self): + return self.backend_record.worklog_fallback_project_id + class ProjectTaskImporter(Component): _name = 'jira.project.task.importer' @@ -172,25 +202,33 @@ def _import(self, binding, **kwargs): return _('Project or issue type is not synchronized.') return super()._import(binding, **kwargs) - def _import_dependencies(self): - """ Import the dependencies for the record""" + def _import_dependency_assignee(self): jira_assignee = self.external_record['fields'].get('assignee') or {} jira_key = jira_assignee.get('key') self._import_dependency(jira_key, 'jira.res.users', record=jira_assignee) + def _import_dependency_issue_type(self): jira_issue_type = self.external_record['fields']['issuetype'] jira_issue_type_id = jira_issue_type['id'] self._import_dependency(jira_issue_type_id, 'jira.issue.type', record=jira_issue_type) + def _import_dependency_parent(self): jira_parent = self.external_record['fields'].get('parent') if jira_parent: jira_parent_id = jira_parent['id'] - self._import_dependency(jira_parent_id, 'jira.project.task', - record=jira_parent) + self._import_dependency(jira_parent_id, 'jira.project.task') + def _import_dependency_epic(self): if self.jira_epic: self._import_dependency(self.jira_epic['id'], 'jira.project.task', record=self.jira_epic) + + def _import_dependencies(self): + """ Import the dependencies for the record""" + self._import_dependency_assignee() + self._import_dependency_issue_type() + self._import_dependency_parent() + self._import_dependency_epic() diff --git a/connector_jira/models/project_task/task_link_jira.py b/connector_jira/models/project_task/task_link_jira.py new file mode 100644 index 000000000..196fb7a7d --- /dev/null +++ b/connector_jira/models/project_task/task_link_jira.py @@ -0,0 +1,96 @@ +# Copyright 2019 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) + +import logging + +from odoo import api, fields, models + +_logger = logging.getLogger(__name__) + + +class TaskLinkJira(models.TransientModel): + _name = 'task.link.jira' + _inherit = 'multi.step.wizard.mixin' + _description = 'Link Task with JIRA' + + task_id = fields.Many2one( + comodel_name='project.task', + name="Task", + required=True, + ondelete='cascade', + ) + jira_key = fields.Char( + string='JIRA Key', + required=True, + ) + backend_id = fields.Many2one( + comodel_name='jira.backend', + string='Jira Backend', + required=True, + ondelete='cascade', + domain="[('id', 'in', linked_backend_ids)]", + ) + linked_backend_ids = fields.Many2many( + comodel_name='jira.backend', + ) + jira_task_id = fields.Many2one( + comodel_name='jira.project.task', + ondelete='cascade', + ) + + @api.model + def _selection_state(self): + return [ + ('start', 'Start'), + ('final', 'Final'), + ] + + @api.model + def default_get(self, fields): + values = super().default_get(fields) + context = self.env.context + if (context.get('active_model') == 'project.task' and + context.get('active_id')): + task = self.env['project.task'].browse(context['active_id']) + project_linked_backends = task.mapped( + 'project_id.jira_bind_ids.backend_id' + ) + values.update({ + 'task_id': task.id, + 'linked_backend_ids': [(6, 0, project_linked_backends.ids)], + }) + if len(project_linked_backends) == 1: + values['backend_id'] = project_linked_backends.id + return values + + def state_exit_start(self): + if not self.jira_task_id: + self._link_binding() + self.state = 'final' + + def _link_binding(self): + with self.backend_id.work_on('jira.project.task') as work: + adapter = work.component(usage='backend.adapter') + with adapter.handle_user_api_errors(): + jira_task = adapter.get(self.jira_key) + self._link_with_jira_task(work, jira_task) + self._run_import_jira_task(work, jira_task) + + def _link_with_jira_task(self, work, jira_task): + values = self._prepare_link_binding_values(jira_task) + self.jira_task_id = self.env['jira.project.task'].create( + values + ) + + def _run_import_jira_task(self, work, jira_task): + importer = work.component(usage="record.importer") + importer.run(jira_task.id, force=True, record=jira_task.raw) + + def _prepare_link_binding_values(self, jira_task): + values = { + 'backend_id': self.backend_id.id, + 'odoo_id': self.task_id.id, + 'jira_key': self.jira_key, + 'external_id': jira_task.id, + } + return values diff --git a/connector_jira/models/queue_job/__init__.py b/connector_jira/models/queue_job/__init__.py new file mode 100644 index 000000000..e4193cf05 --- /dev/null +++ b/connector_jira/models/queue_job/__init__.py @@ -0,0 +1 @@ +from . import common diff --git a/connector_jira/models/queue_job/common.py b/connector_jira/models/queue_job/common.py new file mode 100644 index 000000000..4485331c9 --- /dev/null +++ b/connector_jira/models/queue_job/common.py @@ -0,0 +1,39 @@ +# Copyright 2016-2019 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) + +from odoo import api, models + + +class QueueJob(models.Model): + _inherit = 'queue.job' + + @api.multi + def related_action_jira_link(self): + """Open a jira url for an issue """ + self.ensure_one() + + model_name = self.model_name + # only tested on issues so far + issue_models = ('jira.project.task', 'jira.account.analytic.line') + if model_name not in issue_models: + return + + backend = self.args[0] + jira_id = self.args[1] + + # Get the key of the issue to generate the URI. + # JIRA doesn't have an URI to show an issue by id. + # And at this point, we may be importing a Jira record + # that is not yet imported in Odoo or fails to import, + # so we cannot use the URL computed on the Jira binding. + with backend.work_on('jira.project.task') as work: + adapter = work.component(usage='backend.adapter') + with adapter.handle_user_api_errors(): + jira_record = adapter.get(jira_id) + jira_key = jira_record.key + + return { + 'type': 'ir.actions.act_url', + 'target': 'new', + 'url': backend.make_issue_url(jira_key), + } diff --git a/connector_jira/models/res_users/common.py b/connector_jira/models/res_users/common.py index f08169559..d8bb17a99 100644 --- a/connector_jira/models/res_users/common.py +++ b/connector_jira/models/res_users/common.py @@ -1,4 +1,4 @@ -# Copyright 2016-2018 Camptocamp SA +# Copyright 2016-2019 Camptocamp SA # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) from odoo import _, api, exceptions, fields, models @@ -101,7 +101,8 @@ class UserAdapter(Component): _apply_on = ['jira.res.users'] def read(self, id_): - return self.client.user(id_).raw + with self.handle_404(): + return self.client.user(id_).raw def search(self, fragment=None): """ Search users diff --git a/connector_jira/models/res_users/importer.py b/connector_jira/models/res_users/importer.py index c3af42fbe..aad6b820a 100644 --- a/connector_jira/models/res_users/importer.py +++ b/connector_jira/models/res_users/importer.py @@ -1,7 +1,9 @@ -# Copyright 2016-2018 Camptocamp SA +# Copyright 2016-2019 Camptocamp SA # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) +from odoo import _ from odoo.addons.component.core import Component +from odoo.addons.queue_job.exception import JobError class UserImporter(Component): @@ -21,4 +23,16 @@ def _import(self, binding): ('login', '=', jira_key), ('email', '=', email)], ) + if len(user) > 1: + raise JobError( + _("Several users found (%s) for jira account %s (%s)." + " Please link it manually from the Odoo user's form.") + % (user.mapped('login'), jira_key, email) + ) + elif not user: + raise JobError( + _("No user found for jira account %s (%s)." + " Please link it manually from the Odoo user's form.") + % (jira_key, email) + ) return user.link_with_jira(backends=self.backend_record) diff --git a/connector_jira/tests/__init__.py b/connector_jira/tests/__init__.py new file mode 100644 index 000000000..bf14b1247 --- /dev/null +++ b/connector_jira/tests/__init__.py @@ -0,0 +1,4 @@ +from . import test_auth +from . import test_import_issue_type +from . import test_import_task +from . import test_import_account_analytic_line diff --git a/connector_jira/tests/common.py b/connector_jira/tests/common.py new file mode 100644 index 000000000..aa46011e8 --- /dev/null +++ b/connector_jira/tests/common.py @@ -0,0 +1,272 @@ +# Copyright 2019 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) + +"""Tests for connector_jira + +# Running tests + +Tests are run normally, you can either execute them with odoo's +``--test-enable`` option or using `pytest-odoo +`_ + +The requests to Jira are recorded and simulated using `vcrpy +`_. Which means +the tests can be executed without having a Jira service running. + +However, in order to write new tests or modify the existing ones, you may need +to have a service running to record the Webservice interactions. + +# Recording new tests with vcr.py + +First, you will need a running Jira. We use a docker image, a simple +composition is enough:: + + version: '2' + services: + + jira: + image: cptactionhank/atlassian-jira-software:7.12.3 + volumes: + - "data-jira:/var/atlassian/jira" + ports: + - 8080:8080 + + volumes: + data-jira: + +When you first access to Jira, it will guide you through a procedure +to obtain a demo license. + +Once connected, you will need to do the Oauth dance to obtain tokens. + +You can do so using the CLI command line:: + + odoo jiraoauthdance + +See details in ``connector_jira/cli/jira_oauth_dance.py``. + +Once you have tokens (access+secret), you will need to set them +in environment variables when you run your tests: + +- JIRA_TEST_URL +- JIRA_TEST_TOKEN_ACCESS +- JIRA_TEST_TOKEN_SECRET + +From now on, you can write your tests using the ``recorder.use_cassette`` +decorator or context manager. If you are changing existing tests, you might +need either to manually edit the cassette files in "tests/fixtures/cassettes" +or to record the test again (in such case, IDs may change). + +""" + +import os +import logging +import pprint + +from os.path import dirname, join + +from odoo.addons.component.tests.common import SavepointComponentCase + +from vcr import VCR + +_logger = logging.getLogger(__name__) + +jira_test_url = os.environ.get("JIRA_TEST_URL", "http://jira:8080") +jira_test_token_access = os.environ.get("JIRA_TEST_TOKEN_ACCESS", "") +jira_test_token_secret = os.environ.get("JIRA_TEST_TOKEN_SECRET", "") + + +def get_recorder(**kw): + defaults = dict( + record_mode="once", + cassette_library_dir=join(dirname(__file__), "fixtures/cassettes"), + path_transformer=VCR.ensure_suffix(".yaml"), + match_on=["method", "path", "query"], + filter_headers=["Authorization"], + decode_compressed_response=True, + ) + defaults.update(kw) + return VCR(**defaults) + + +recorder = get_recorder() + + +class JiraTransactionCase(SavepointComponentCase): + """Base class for tests with Jira""" + + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.backend_record = cls.env.ref("connector_jira.jira_backend_demo") + cls.backend_record.write( + { + "uri": jira_test_url, + "access_token": jira_test_token_access, + "access_secret": jira_test_token_secret, + "epic_link_field_name": "customfield_10101", + } + ) + + _base_issue_types = [ + ("Task", "10002"), + ("Sub-task", "10003"), + ("Story", "10001"), + ("Bug", "10004"), + ("Epic", "10000"), + ] + + @classmethod + def _link_user(cls, user, jira_login): + return ( + cls.env["jira.res.users"] + .with_context(no_connector_export=True) + .create( + { + "odoo_id": user.id, + "backend_id": cls.backend_record.id, + "external_id": jira_login, + } + ) + ) + + @classmethod + def _create_issue_type_bindings(cls): + for name, jira_id in cls._base_issue_types: + cls.env["jira.issue.type"].create( + { + "name": name, + "backend_id": cls.backend_record.id, + "external_id": jira_id, + } + ) + + @classmethod + def _create_project_binding( + cls, project, sync_action="link", issue_types=None, **extra + ): + values = { + "odoo_id": project.id, + "jira_key": "TEST", + "sync_action": sync_action, + "backend_id": cls.backend_record.id, + # dummy id + "external_id": "9999", + } + if issue_types: + values.update({"sync_issue_type_ids": [(6, 0, issue_types.ids)]}) + values.update(**extra) + return ( + cls.env["jira.project.project"] + .with_context(no_connector_export=True) + .create(values) + ) + + @classmethod + def _create_task_binding(cls, task, **extra): + values = { + "odoo_id": task.id, + "jira_key": "TEST", + "backend_id": cls.backend_record.id, + # dummy id + "external_id": "9999", + } + values.update(**extra) + return ( + cls.env["jira.project.task"] + .with_context(no_connector_export=True) + .create(values) + ) + + # copy of 12.0's method, with 'black' applied + # https://github.com/odoo/odoo/blob/251021796c25676f5c341e0a3fc3ba4c9feb85bb/odoo/tests/common.py#L294 + # to remove when migrating to 12.0 + def assertRecordValues(self, records, expected_values): + """ Compare a recordset with a list of dictionaries representing the + expected results. + This method performs a comparison element by element based on their + index. Then, the order of the expected values is extremely important. + Note that: + - Comparison between falsy values is supported: False match with + None. + - Comparison between monetary field is also treated according the + currency's rounding. + - Comparison between x2many field is done by ids. Then, empty + expected ids must be []. + - Comparison between many2one field id done by id. Empty comparison + can be done using any falsy value. + :param records: The records to compare. + :param expected_values: List of dicts expected to be exactly matched in + records + """ + + def _compare_candidate(record, candidate): + """ Return True if the candidate matches the given record """ + for field_name in candidate.keys(): + record_value = record[field_name] + candidate_value = candidate[field_name] + field_type = record._fields[field_name].type + if field_type == "monetary": + # Compare monetary field. + currency_field_name = record._fields[ + field_name + ].currency_field + record_currency = record[currency_field_name] + if ( + record_currency.compare_amounts( + candidate_value, record_value + ) + if record_currency + else candidate_value != record_value + ): + return False + elif field_type in ("one2many", "many2many"): + # Compare x2many relational fields. + # Empty comparison must be an empty list to be True. + if set(record_value.ids) != set(candidate_value): + return False + elif field_type == "many2one": + # Compare many2one relational fields. + # Every falsy value is allowed to compare with an empty + # record. + if ( + record_value or candidate_value + ) and record_value.id != candidate_value: + return False + elif ( + candidate_value or record_value + ) and record_value != candidate_value: + # Compare others fields if not both interpreted as falsy + # values. + return False + return True + + def _format_message(records, expected_values): + """ Return a formatted representation of records/expected_values. + """ + all_records_values = records.read( + list(expected_values[0].keys()), load=False + ) + msg1 = "\n".join(pprint.pformat(dic) for dic in all_records_values) + msg2 = "\n".join(pprint.pformat(dic) for dic in expected_values) + return "Current values:\n\n%s\n\nExpected values:\n\n%s" % ( + msg1, + msg2, + ) + + # if the length or both things to compare is different, we can already + # tell they're not equal + if len(records) != len(expected_values): + msg = "Wrong number of records to compare: %d != %d.\n\n" % ( + len(records), + len(expected_values), + ) + self.fail(msg + _format_message(records, expected_values)) + + for index, record in enumerate(records): + if not _compare_candidate(record, expected_values[index]): + msg = ( + "Record doesn't match expected values at index %d.\n\n" + % index + ) + self.fail(msg + _format_message(records, expected_values)) diff --git a/connector_jira/tests/fixtures/cassettes/test_auth_check_connection.yaml b/connector_jira/tests/fixtures/cassettes/test_auth_check_connection.yaml new file mode 100644 index 000000000..83cb89c20 --- /dev/null +++ b/connector_jira/tests/fixtures/cassettes/test_auth_check_connection.yaml @@ -0,0 +1,295 @@ +interactions: +- request: + body: null + headers: + Accept: + - !!binary | + YXBwbGljYXRpb24vanNvbiwqLio7cT0wLjk= + Accept-Encoding: + - !!binary | + Z3ppcCwgZGVmbGF0ZQ== + Cache-Control: + - !!binary | + bm8tY2FjaGU= + Connection: + - !!binary | + a2VlcC1hbGl2ZQ== + Content-Type: + - !!binary | + YXBwbGljYXRpb24vanNvbg== + User-Agent: + - !!binary | + cHl0aG9uLXJlcXVlc3RzLzIuMTkuMQ== + X-Atlassian-Token: + - !!binary | + bm8tY2hlY2s= + method: GET + uri: http://jira:8080/rest/api/2/serverInfo + response: + body: {string: '{"baseUrl":"http://localhost:8080","version":"7.12.3","versionNumbers":[7,12,3],"deploymentType":"Server","buildNumber":712004,"buildDate":"2018-10-12T00:00:00.000+0000","serverTime":"2019-04-03T16:07:19.756+0000","scmInfo":"5ef91d760d7124da5ebec5c16a948a4a807698df","serverTitle":"Jira"}'} + headers: + Cache-Control: ['no-cache, no-store, no-transform'] + Content-Security-Policy: [frame-ancestors 'self'] + Content-Type: [application/json;charset=UTF-8] + Date: ['Wed, 03 Apr 2019 16:07:19 GMT'] + Set-Cookie: [JSESSIONID=3137F318B729C7876F866A7B49A5A474; Path=/; HttpOnly, + atlassian.xsrf.token=BYG3-6SPF-0UM1-2LBO_2aad58b7529761dd022e19fb8654325b4b0f8859_lin; + Path=/] + Transfer-Encoding: [chunked] + Vary: [User-Agent] + X-AREQUESTID: [967x756x1] + X-ASEN: [SEN-L13384799] + X-ASESSIONID: [109kqxb] + X-AUSERNAME: [gbaconnier] + X-Content-Type-Options: [nosniff] + X-Frame-Options: [SAMEORIGIN] + X-Seraph-LoginReason: [OK] + X-XSS-Protection: [1; mode=block] + content-length: ['288'] + status: {code: 200, message: ''} +- request: + body: null + headers: + Accept: + - !!binary | + YXBwbGljYXRpb24vanNvbiwqLio7cT0wLjk= + Accept-Encoding: + - !!binary | + Z3ppcCwgZGVmbGF0ZQ== + Cache-Control: + - !!binary | + bm8tY2FjaGU= + Connection: + - !!binary | + a2VlcC1hbGl2ZQ== + Content-Type: + - !!binary | + YXBwbGljYXRpb24vanNvbg== + Cookie: + - !!binary | + YXRsYXNzaWFuLnhzcmYudG9rZW49QllHMy02U1BGLTBVTTEtMkxCT18yYWFkNThiNzUyOTc2MWRk + MDIyZTE5ZmI4NjU0MzI1YjRiMGY4ODU5X2xpbjsgSlNFU1NJT05JRD0zMTM3RjMxOEI3MjlDNzg3 + NkY4NjZBN0I0OUE1QTQ3NA== + User-Agent: + - !!binary | + cHl0aG9uLXJlcXVlc3RzLzIuMTkuMQ== + X-Atlassian-Token: + - !!binary | + bm8tY2hlY2s= + method: GET + uri: http://jira:8080/rest/api/2/field + response: + body: {string: "[{\"id\":\"issuetype\",\"name\":\"Issue Type\",\"custom\":false,\"\ + orderable\":true,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"\ + issuetype\",\"type\"],\"schema\":{\"type\":\"issuetype\",\"system\":\"issuetype\"\ + }},{\"id\":\"components\",\"name\":\"Component/s\",\"custom\":false,\"orderable\"\ + :true,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"component\"\ + ],\"schema\":{\"type\":\"array\",\"items\":\"component\",\"system\":\"components\"\ + }},{\"id\":\"issuekey\",\"name\":\"Key\",\"custom\":false,\"orderable\":false,\"\ + navigable\":true,\"searchable\":false,\"clauseNames\":[\"id\",\"issue\",\"\ + issuekey\",\"key\"]},{\"id\":\"timespent\",\"name\":\"Time Spent\",\"custom\"\ + :false,\"orderable\":false,\"navigable\":true,\"searchable\":false,\"clauseNames\"\ + :[\"timespent\"],\"schema\":{\"type\":\"number\",\"system\":\"timespent\"\ + }},{\"id\":\"timeoriginalestimate\",\"name\":\"Original Estimate\",\"custom\"\ + :false,\"orderable\":false,\"navigable\":true,\"searchable\":false,\"clauseNames\"\ + :[\"originalEstimate\",\"timeoriginalestimate\"],\"schema\":{\"type\":\"number\"\ + ,\"system\":\"timeoriginalestimate\"}},{\"id\":\"project\",\"name\":\"Project\"\ + ,\"custom\":false,\"orderable\":false,\"navigable\":true,\"searchable\":true,\"\ + clauseNames\":[\"project\"],\"schema\":{\"type\":\"project\",\"system\":\"\ + project\"}},{\"id\":\"description\",\"name\":\"Description\",\"custom\":false,\"\ + orderable\":true,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"\ + description\"],\"schema\":{\"type\":\"string\",\"system\":\"description\"\ + }},{\"id\":\"fixVersions\",\"name\":\"Fix Version/s\",\"custom\":false,\"\ + orderable\":true,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"\ + fixVersion\"],\"schema\":{\"type\":\"array\",\"items\":\"version\",\"system\"\ + :\"fixVersions\"}},{\"id\":\"aggregatetimespent\",\"name\":\"\u03A3 Time Spent\"\ + ,\"custom\":false,\"orderable\":false,\"navigable\":true,\"searchable\":false,\"\ + clauseNames\":[],\"schema\":{\"type\":\"number\",\"system\":\"aggregatetimespent\"\ + }},{\"id\":\"resolution\",\"name\":\"Resolution\",\"custom\":false,\"orderable\"\ + :true,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"resolution\"\ + ],\"schema\":{\"type\":\"resolution\",\"system\":\"resolution\"}},{\"id\"\ + :\"timetracking\",\"name\":\"Time Tracking\",\"custom\":false,\"orderable\"\ + :true,\"navigable\":false,\"searchable\":true,\"clauseNames\":[],\"schema\"\ + :{\"type\":\"timetracking\",\"system\":\"timetracking\"}},{\"id\":\"customfield_10104\"\ + ,\"name\":\"Epic Color\",\"custom\":true,\"orderable\":true,\"navigable\"\ + :true,\"searchable\":true,\"clauseNames\":[\"cf[10104]\",\"Epic Color\"],\"\ + schema\":{\"type\":\"string\",\"custom\":\"com.pyxis.greenhopper.jira:gh-epic-color\"\ + ,\"customId\":10104}},{\"id\":\"customfield_10105\",\"name\":\"Rank\",\"custom\"\ + :true,\"orderable\":true,\"navigable\":true,\"searchable\":true,\"clauseNames\"\ + :[\"cf[10105]\",\"Rank\"],\"schema\":{\"type\":\"any\",\"custom\":\"com.pyxis.greenhopper.jira:gh-lexo-rank\"\ + ,\"customId\":10105}},{\"id\":\"security\",\"name\":\"Security Level\",\"\ + custom\":false,\"orderable\":true,\"navigable\":true,\"searchable\":true,\"\ + clauseNames\":[\"level\"],\"schema\":{\"type\":\"securitylevel\",\"system\"\ + :\"security\"}},{\"id\":\"customfield_10106\",\"name\":\"Story Points\",\"\ + custom\":true,\"orderable\":true,\"navigable\":true,\"searchable\":true,\"\ + clauseNames\":[\"cf[10106]\",\"Story Points\"],\"schema\":{\"type\":\"number\"\ + ,\"custom\":\"com.atlassian.jira.plugin.system.customfieldtypes:float\",\"\ + customId\":10106}},{\"id\":\"attachment\",\"name\":\"Attachment\",\"custom\"\ + :false,\"orderable\":true,\"navigable\":false,\"searchable\":true,\"clauseNames\"\ + :[\"attachments\"],\"schema\":{\"type\":\"array\",\"items\":\"attachment\"\ + ,\"system\":\"attachment\"}},{\"id\":\"aggregatetimeestimate\",\"name\":\"\ + \u03A3 Remaining Estimate\",\"custom\":false,\"orderable\":false,\"navigable\"\ + :true,\"searchable\":false,\"clauseNames\":[],\"schema\":{\"type\":\"number\"\ + ,\"system\":\"aggregatetimeestimate\"}},{\"id\":\"resolutiondate\",\"name\"\ + :\"Resolved\",\"custom\":false,\"orderable\":false,\"navigable\":true,\"searchable\"\ + :true,\"clauseNames\":[\"resolutiondate\",\"resolved\"],\"schema\":{\"type\"\ + :\"datetime\",\"system\":\"resolutiondate\"}},{\"id\":\"workratio\",\"name\"\ + :\"Work Ratio\",\"custom\":false,\"orderable\":false,\"navigable\":true,\"\ + searchable\":true,\"clauseNames\":[\"workratio\"],\"schema\":{\"type\":\"\ + number\",\"system\":\"workratio\"}},{\"id\":\"summary\",\"name\":\"Summary\"\ + ,\"custom\":false,\"orderable\":true,\"navigable\":true,\"searchable\":true,\"\ + clauseNames\":[\"summary\"],\"schema\":{\"type\":\"string\",\"system\":\"\ + summary\"}},{\"id\":\"lastViewed\",\"name\":\"Last Viewed\",\"custom\":false,\"\ + orderable\":false,\"navigable\":true,\"searchable\":false,\"clauseNames\"\ + :[\"lastViewed\"],\"schema\":{\"type\":\"datetime\",\"system\":\"lastViewed\"\ + }},{\"id\":\"watches\",\"name\":\"Watchers\",\"custom\":false,\"orderable\"\ + :false,\"navigable\":true,\"searchable\":false,\"clauseNames\":[\"watchers\"\ + ],\"schema\":{\"type\":\"watches\",\"system\":\"watches\"}},{\"id\":\"creator\"\ + ,\"name\":\"Creator\",\"custom\":false,\"orderable\":false,\"navigable\":true,\"\ + searchable\":true,\"clauseNames\":[\"creator\"],\"schema\":{\"type\":\"user\"\ + ,\"system\":\"creator\"}},{\"id\":\"thumbnail\",\"name\":\"Images\",\"custom\"\ + :false,\"orderable\":false,\"navigable\":true,\"searchable\":false,\"clauseNames\"\ + :[]},{\"id\":\"subtasks\",\"name\":\"Sub-Tasks\",\"custom\":false,\"orderable\"\ + :false,\"navigable\":true,\"searchable\":false,\"clauseNames\":[\"subtasks\"\ + ],\"schema\":{\"type\":\"array\",\"items\":\"issuelinks\",\"system\":\"subtasks\"\ + }},{\"id\":\"created\",\"name\":\"Created\",\"custom\":false,\"orderable\"\ + :false,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"created\"\ + ,\"createdDate\"],\"schema\":{\"type\":\"datetime\",\"system\":\"created\"\ + }},{\"id\":\"reporter\",\"name\":\"Reporter\",\"custom\":false,\"orderable\"\ + :true,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"reporter\"\ + ],\"schema\":{\"type\":\"user\",\"system\":\"reporter\"}},{\"id\":\"aggregateprogress\"\ + ,\"name\":\"\u03A3 Progress\",\"custom\":false,\"orderable\":false,\"navigable\"\ + :true,\"searchable\":false,\"clauseNames\":[],\"schema\":{\"type\":\"progress\"\ + ,\"system\":\"aggregateprogress\"}},{\"id\":\"customfield_10000\",\"name\"\ + :\"Development\",\"custom\":true,\"orderable\":true,\"navigable\":true,\"\ + searchable\":true,\"clauseNames\":[\"cf[10000]\",\"Development\"],\"schema\"\ + :{\"type\":\"any\",\"custom\":\"com.atlassian.jira.plugins.jira-development-integration-plugin:devsummary\"\ + ,\"customId\":10000}},{\"id\":\"priority\",\"name\":\"Priority\",\"custom\"\ + :false,\"orderable\":true,\"navigable\":true,\"searchable\":true,\"clauseNames\"\ + :[\"priority\"],\"schema\":{\"type\":\"priority\",\"system\":\"priority\"\ + }},{\"id\":\"customfield_10100\",\"name\":\"Sprint\",\"custom\":true,\"orderable\"\ + :true,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"cf[10100]\"\ + ,\"Sprint\"],\"schema\":{\"type\":\"array\",\"items\":\"string\",\"custom\"\ + :\"com.pyxis.greenhopper.jira:gh-sprint\",\"customId\":10100}},{\"id\":\"\ + customfield_10101\",\"name\":\"Epic Link\",\"custom\":true,\"orderable\":true,\"\ + navigable\":true,\"searchable\":true,\"clauseNames\":[\"cf[10101]\",\"Epic\ + \ Link\"],\"schema\":{\"type\":\"any\",\"custom\":\"com.pyxis.greenhopper.jira:gh-epic-link\"\ + ,\"customId\":10101}},{\"id\":\"customfield_10102\",\"name\":\"Epic Status\"\ + ,\"custom\":true,\"orderable\":true,\"navigable\":true,\"searchable\":true,\"\ + clauseNames\":[\"cf[10102]\",\"Epic Status\"],\"schema\":{\"type\":\"option\"\ + ,\"custom\":\"com.pyxis.greenhopper.jira:gh-epic-status\",\"customId\":10102}},{\"\ + id\":\"labels\",\"name\":\"Labels\",\"custom\":false,\"orderable\":true,\"\ + navigable\":true,\"searchable\":true,\"clauseNames\":[\"labels\"],\"schema\"\ + :{\"type\":\"array\",\"items\":\"string\",\"system\":\"labels\"}},{\"id\"\ + :\"customfield_10103\",\"name\":\"Epic Name\",\"custom\":true,\"orderable\"\ + :true,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"cf[10103]\"\ + ,\"Epic Name\"],\"schema\":{\"type\":\"string\",\"custom\":\"com.pyxis.greenhopper.jira:gh-epic-label\"\ + ,\"customId\":10103}},{\"id\":\"environment\",\"name\":\"Environment\",\"\ + custom\":false,\"orderable\":true,\"navigable\":true,\"searchable\":true,\"\ + clauseNames\":[\"environment\"],\"schema\":{\"type\":\"string\",\"system\"\ + :\"environment\"}},{\"id\":\"timeestimate\",\"name\":\"Remaining Estimate\"\ + ,\"custom\":false,\"orderable\":false,\"navigable\":true,\"searchable\":false,\"\ + clauseNames\":[\"remainingEstimate\",\"timeestimate\"],\"schema\":{\"type\"\ + :\"number\",\"system\":\"timeestimate\"}},{\"id\":\"aggregatetimeoriginalestimate\"\ + ,\"name\":\"\u03A3 Original Estimate\",\"custom\":false,\"orderable\":false,\"\ + navigable\":true,\"searchable\":false,\"clauseNames\":[],\"schema\":{\"type\"\ + :\"number\",\"system\":\"aggregatetimeoriginalestimate\"}},{\"id\":\"versions\"\ + ,\"name\":\"Affects Version/s\",\"custom\":false,\"orderable\":true,\"navigable\"\ + :true,\"searchable\":true,\"clauseNames\":[\"affectedVersion\"],\"schema\"\ + :{\"type\":\"array\",\"items\":\"version\",\"system\":\"versions\"}},{\"id\"\ + :\"duedate\",\"name\":\"Due Date\",\"custom\":false,\"orderable\":true,\"\ + navigable\":true,\"searchable\":true,\"clauseNames\":[\"due\",\"duedate\"\ + ],\"schema\":{\"type\":\"date\",\"system\":\"duedate\"}},{\"id\":\"progress\"\ + ,\"name\":\"Progress\",\"custom\":false,\"orderable\":false,\"navigable\"\ + :true,\"searchable\":false,\"clauseNames\":[\"progress\"],\"schema\":{\"type\"\ + :\"progress\",\"system\":\"progress\"}},{\"id\":\"comment\",\"name\":\"Comment\"\ + ,\"custom\":false,\"orderable\":true,\"navigable\":false,\"searchable\":true,\"\ + clauseNames\":[\"comment\"],\"schema\":{\"type\":\"comments-page\",\"system\"\ + :\"comment\"}},{\"id\":\"votes\",\"name\":\"Votes\",\"custom\":false,\"orderable\"\ + :false,\"navigable\":true,\"searchable\":false,\"clauseNames\":[\"votes\"\ + ],\"schema\":{\"type\":\"votes\",\"system\":\"votes\"}},{\"id\":\"issuelinks\"\ + ,\"name\":\"Linked Issues\",\"custom\":false,\"orderable\":true,\"navigable\"\ + :true,\"searchable\":true,\"clauseNames\":[],\"schema\":{\"type\":\"array\"\ + ,\"items\":\"issuelinks\",\"system\":\"issuelinks\"}},{\"id\":\"worklog\"\ + ,\"name\":\"Log Work\",\"custom\":false,\"orderable\":true,\"navigable\":false,\"\ + searchable\":true,\"clauseNames\":[],\"schema\":{\"type\":\"array\",\"items\"\ + :\"worklog\",\"system\":\"worklog\"}},{\"id\":\"assignee\",\"name\":\"Assignee\"\ + ,\"custom\":false,\"orderable\":true,\"navigable\":true,\"searchable\":true,\"\ + clauseNames\":[\"assignee\"],\"schema\":{\"type\":\"user\",\"system\":\"assignee\"\ + }},{\"id\":\"updated\",\"name\":\"Updated\",\"custom\":false,\"orderable\"\ + :false,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"updated\"\ + ,\"updatedDate\"],\"schema\":{\"type\":\"datetime\",\"system\":\"updated\"\ + }},{\"id\":\"status\",\"name\":\"Status\",\"custom\":false,\"orderable\":false,\"\ + navigable\":true,\"searchable\":true,\"clauseNames\":[\"status\"],\"schema\"\ + :{\"type\":\"status\",\"system\":\"status\"}}]"} + headers: + Cache-Control: ['no-cache, no-store, no-transform'] + Content-Security-Policy: [frame-ancestors 'self'] + Content-Type: [application/json;charset=UTF-8] + Date: ['Wed, 03 Apr 2019 16:07:19 GMT'] + Set-Cookie: [JSESSIONID=1CF7A0A7B49089B08A6BB1723E4732BD; Path=/; HttpOnly] + Transfer-Encoding: [chunked] + Vary: [User-Agent] + X-AREQUESTID: [967x757x1] + X-ASEN: [SEN-L13384799] + X-ASESSIONID: [7pffyo] + X-AUSERNAME: [gbaconnier] + X-Content-Type-Options: [nosniff] + X-Frame-Options: [SAMEORIGIN] + X-Seraph-LoginReason: [OK] + X-XSS-Protection: [1; mode=block] + content-length: ['9557'] + status: {code: 200, message: ''} +- request: + body: null + headers: + Accept: + - !!binary | + YXBwbGljYXRpb24vanNvbiwqLio7cT0wLjk= + Accept-Encoding: + - !!binary | + Z3ppcCwgZGVmbGF0ZQ== + Cache-Control: + - !!binary | + bm8tY2FjaGU= + Connection: + - !!binary | + a2VlcC1hbGl2ZQ== + Content-Type: + - !!binary | + YXBwbGljYXRpb24vanNvbg== + Cookie: + - !!binary | + YXRsYXNzaWFuLnhzcmYudG9rZW49QllHMy02U1BGLTBVTTEtMkxCT18yYWFkNThiNzUyOTc2MWRk + MDIyZTE5ZmI4NjU0MzI1YjRiMGY4ODU5X2xpbjsgSlNFU1NJT05JRD0xQ0Y3QTBBN0I0OTA4OUIw + OEE2QkIxNzIzRTQ3MzJCRA== + User-Agent: + - !!binary | + cHl0aG9uLXJlcXVlc3RzLzIuMTkuMQ== + X-Atlassian-Token: + - !!binary | + bm8tY2hlY2s= + method: GET + uri: http://jira:8080/rest/api/2/myself + response: + body: {string: '{"self":"http://jira:8080/rest/api/2/user?username=gbaconnier","key":"gbaconnier","name":"gbaconnier","emailAddress":"guewen.baconnier@camptocamp.com","avatarUrls":{"48x48":"https://www.gravatar.com/avatar/ad252192c3f73885676b7d2e850ad13c?d=mm&s=48","24x24":"https://www.gravatar.com/avatar/ad252192c3f73885676b7d2e850ad13c?d=mm&s=24","16x16":"https://www.gravatar.com/avatar/ad252192c3f73885676b7d2e850ad13c?d=mm&s=16","32x32":"https://www.gravatar.com/avatar/ad252192c3f73885676b7d2e850ad13c?d=mm&s=32"},"displayName":"Guewen + Baconnier","active":true,"timeZone":"GMT","locale":"en_US","groups":{"size":2,"items":[]},"applicationRoles":{"size":1,"items":[]},"expand":"groups,applicationRoles"}'} + headers: + Cache-Control: ['no-cache, no-store, no-transform'] + Content-Security-Policy: [frame-ancestors 'self'] + Content-Type: [application/json;charset=UTF-8] + Date: ['Wed, 03 Apr 2019 16:07:19 GMT'] + Set-Cookie: [JSESSIONID=6149484F0565750836475A1F1B3FA362; Path=/; HttpOnly] + Transfer-Encoding: [chunked] + Vary: [User-Agent] + X-AREQUESTID: [967x758x1] + X-ASEN: [SEN-L13384799] + X-ASESSIONID: [ywtrwx] + X-AUSERNAME: [gbaconnier] + X-Content-Type-Options: [nosniff] + X-Frame-Options: [SAMEORIGIN] + X-Seraph-LoginReason: [OK] + X-XSS-Protection: [1; mode=block] + content-length: ['694'] + status: {code: 200, message: ''} +version: 1 diff --git a/connector_jira/tests/fixtures/cassettes/test_auth_check_connection_failure.yaml b/connector_jira/tests/fixtures/cassettes/test_auth_check_connection_failure.yaml new file mode 100644 index 000000000..42f433d32 --- /dev/null +++ b/connector_jira/tests/fixtures/cassettes/test_auth_check_connection_failure.yaml @@ -0,0 +1,355 @@ +interactions: +- request: + body: null + headers: + Accept: + - !!binary | + YXBwbGljYXRpb24vanNvbiwqLio7cT0wLjk= + Accept-Encoding: + - !!binary | + Z3ppcCwgZGVmbGF0ZQ== + Cache-Control: + - !!binary | + bm8tY2FjaGU= + Connection: + - !!binary | + a2VlcC1hbGl2ZQ== + Content-Type: + - !!binary | + YXBwbGljYXRpb24vanNvbg== + User-Agent: + - !!binary | + cHl0aG9uLXJlcXVlc3RzLzIuMjEuMA== + X-Atlassian-Token: + - !!binary | + bm8tY2hlY2s= + method: GET + uri: http://jira:8080/rest/api/2/serverInfo + response: + body: {string: '{"baseUrl":"http://localhost:8080","version":"7.12.3","versionNumbers":[7,12,3],"deploymentType":"Server","buildNumber":712004,"buildDate":"2018-10-12T00:00:00.000+0000","scmInfo":"5ef91d760d7124da5ebec5c16a948a4a807698df","serverTitle":"Jira"}'} + headers: + Cache-Control: ['no-cache, no-store, no-transform'] + Content-Security-Policy: [frame-ancestors 'self'] + Content-Type: [application/json;charset=UTF-8] + Date: ['Thu, 04 Apr 2019 11:39:00 GMT'] + Set-Cookie: [atlassian.xsrf.token=BYG3-6SPF-0UM1-2LBO_24361ed8c1e67b559e8a490045880a8bfdf361a1_lout; + Path=/] + Transfer-Encoding: [chunked] + Vary: [User-Agent] + X-AREQUESTID: [699x1203x1] + X-ASEN: [SEN-L13384799] + X-AUSERNAME: [anonymous] + X-Content-Type-Options: [nosniff] + X-Frame-Options: [SAMEORIGIN] + X-XSS-Protection: [1; mode=block] + content-length: ['244'] + status: {code: 200, message: ''} +- request: + body: null + headers: + Accept: + - !!binary | + YXBwbGljYXRpb24vanNvbiwqLio7cT0wLjk= + Accept-Encoding: + - !!binary | + Z3ppcCwgZGVmbGF0ZQ== + Cache-Control: + - !!binary | + bm8tY2FjaGU= + Connection: + - !!binary | + a2VlcC1hbGl2ZQ== + Content-Type: + - !!binary | + YXBwbGljYXRpb24vanNvbg== + Cookie: + - !!binary | + YXRsYXNzaWFuLnhzcmYudG9rZW49QllHMy02U1BGLTBVTTEtMkxCT18yNDM2MWVkOGMxZTY3YjU1 + OWU4YTQ5MDA0NTg4MGE4YmZkZjM2MWExX2xvdXQ= + User-Agent: + - !!binary | + cHl0aG9uLXJlcXVlc3RzLzIuMjEuMA== + X-Atlassian-Token: + - !!binary | + bm8tY2hlY2s= + method: GET + uri: http://jira:8080/rest/api/2/field + response: + body: {string: "[{\"id\":\"issuekey\",\"name\":\"Key\",\"custom\":false,\"orderable\"\ + :false,\"navigable\":true,\"searchable\":false,\"clauseNames\":[\"id\",\"\ + issue\",\"issuekey\",\"key\"]},{\"id\":\"timespent\",\"name\":\"Time Spent\"\ + ,\"custom\":false,\"orderable\":false,\"navigable\":true,\"searchable\":false,\"\ + clauseNames\":[\"timespent\"],\"schema\":{\"type\":\"number\",\"system\":\"\ + timespent\"}},{\"id\":\"timeoriginalestimate\",\"name\":\"Original Estimate\"\ + ,\"custom\":false,\"orderable\":false,\"navigable\":true,\"searchable\":false,\"\ + clauseNames\":[\"originalEstimate\",\"timeoriginalestimate\"],\"schema\":{\"\ + type\":\"number\",\"system\":\"timeoriginalestimate\"}},{\"id\":\"project\"\ + ,\"name\":\"Project\",\"custom\":false,\"orderable\":false,\"navigable\":true,\"\ + searchable\":true,\"clauseNames\":[\"project\"],\"schema\":{\"type\":\"project\"\ + ,\"system\":\"project\"}},{\"id\":\"aggregatetimespent\",\"name\":\"\u03A3\ + \ Time Spent\",\"custom\":false,\"orderable\":false,\"navigable\":true,\"\ + searchable\":false,\"clauseNames\":[],\"schema\":{\"type\":\"number\",\"system\"\ + :\"aggregatetimespent\"}},{\"id\":\"timetracking\",\"name\":\"Time Tracking\"\ + ,\"custom\":false,\"orderable\":true,\"navigable\":false,\"searchable\":true,\"\ + clauseNames\":[],\"schema\":{\"type\":\"timetracking\",\"system\":\"timetracking\"\ + }},{\"id\":\"attachment\",\"name\":\"Attachment\",\"custom\":false,\"orderable\"\ + :true,\"navigable\":false,\"searchable\":true,\"clauseNames\":[\"attachments\"\ + ],\"schema\":{\"type\":\"array\",\"items\":\"attachment\",\"system\":\"attachment\"\ + }},{\"id\":\"aggregatetimeestimate\",\"name\":\"\u03A3 Remaining Estimate\"\ + ,\"custom\":false,\"orderable\":false,\"navigable\":true,\"searchable\":false,\"\ + clauseNames\":[],\"schema\":{\"type\":\"number\",\"system\":\"aggregatetimeestimate\"\ + }},{\"id\":\"resolutiondate\",\"name\":\"Resolved\",\"custom\":false,\"orderable\"\ + :false,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"resolutiondate\"\ + ,\"resolved\"],\"schema\":{\"type\":\"datetime\",\"system\":\"resolutiondate\"\ + }},{\"id\":\"workratio\",\"name\":\"Work Ratio\",\"custom\":false,\"orderable\"\ + :false,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"workratio\"\ + ],\"schema\":{\"type\":\"number\",\"system\":\"workratio\"}},{\"id\":\"lastViewed\"\ + ,\"name\":\"Last Viewed\",\"custom\":false,\"orderable\":false,\"navigable\"\ + :true,\"searchable\":false,\"clauseNames\":[\"lastViewed\"],\"schema\":{\"\ + type\":\"datetime\",\"system\":\"lastViewed\"}},{\"id\":\"watches\",\"name\"\ + :\"Watchers\",\"custom\":false,\"orderable\":false,\"navigable\":true,\"searchable\"\ + :false,\"clauseNames\":[\"watchers\"],\"schema\":{\"type\":\"watches\",\"\ + system\":\"watches\"}},{\"id\":\"creator\",\"name\":\"Creator\",\"custom\"\ + :false,\"orderable\":false,\"navigable\":true,\"searchable\":true,\"clauseNames\"\ + :[\"creator\"],\"schema\":{\"type\":\"user\",\"system\":\"creator\"}},{\"\ + id\":\"thumbnail\",\"name\":\"Images\",\"custom\":false,\"orderable\":false,\"\ + navigable\":true,\"searchable\":false,\"clauseNames\":[]},{\"id\":\"subtasks\"\ + ,\"name\":\"Sub-Tasks\",\"custom\":false,\"orderable\":false,\"navigable\"\ + :true,\"searchable\":false,\"clauseNames\":[\"subtasks\"],\"schema\":{\"type\"\ + :\"array\",\"items\":\"issuelinks\",\"system\":\"subtasks\"}},{\"id\":\"created\"\ + ,\"name\":\"Created\",\"custom\":false,\"orderable\":false,\"navigable\":true,\"\ + searchable\":true,\"clauseNames\":[\"created\",\"createdDate\"],\"schema\"\ + :{\"type\":\"datetime\",\"system\":\"created\"}},{\"id\":\"aggregateprogress\"\ + ,\"name\":\"\u03A3 Progress\",\"custom\":false,\"orderable\":false,\"navigable\"\ + :true,\"searchable\":false,\"clauseNames\":[],\"schema\":{\"type\":\"progress\"\ + ,\"system\":\"aggregateprogress\"}},{\"id\":\"timeestimate\",\"name\":\"Remaining\ + \ Estimate\",\"custom\":false,\"orderable\":false,\"navigable\":true,\"searchable\"\ + :false,\"clauseNames\":[\"remainingEstimate\",\"timeestimate\"],\"schema\"\ + :{\"type\":\"number\",\"system\":\"timeestimate\"}},{\"id\":\"aggregatetimeoriginalestimate\"\ + ,\"name\":\"\u03A3 Original Estimate\",\"custom\":false,\"orderable\":false,\"\ + navigable\":true,\"searchable\":false,\"clauseNames\":[],\"schema\":{\"type\"\ + :\"number\",\"system\":\"aggregatetimeoriginalestimate\"}},{\"id\":\"progress\"\ + ,\"name\":\"Progress\",\"custom\":false,\"orderable\":false,\"navigable\"\ + :true,\"searchable\":false,\"clauseNames\":[\"progress\"],\"schema\":{\"type\"\ + :\"progress\",\"system\":\"progress\"}},{\"id\":\"comment\",\"name\":\"Comment\"\ + ,\"custom\":false,\"orderable\":true,\"navigable\":false,\"searchable\":true,\"\ + clauseNames\":[\"comment\"],\"schema\":{\"type\":\"comments-page\",\"system\"\ + :\"comment\"}},{\"id\":\"votes\",\"name\":\"Votes\",\"custom\":false,\"orderable\"\ + :false,\"navigable\":true,\"searchable\":false,\"clauseNames\":[\"votes\"\ + ],\"schema\":{\"type\":\"votes\",\"system\":\"votes\"}},{\"id\":\"worklog\"\ + ,\"name\":\"Log Work\",\"custom\":false,\"orderable\":true,\"navigable\":false,\"\ + searchable\":true,\"clauseNames\":[],\"schema\":{\"type\":\"array\",\"items\"\ + :\"worklog\",\"system\":\"worklog\"}},{\"id\":\"updated\",\"name\":\"Updated\"\ + ,\"custom\":false,\"orderable\":false,\"navigable\":true,\"searchable\":true,\"\ + clauseNames\":[\"updated\",\"updatedDate\"],\"schema\":{\"type\":\"datetime\"\ + ,\"system\":\"updated\"}},{\"id\":\"status\",\"name\":\"Status\",\"custom\"\ + :false,\"orderable\":false,\"navigable\":true,\"searchable\":true,\"clauseNames\"\ + :[\"status\"],\"schema\":{\"type\":\"status\",\"system\":\"status\"}}]"} + headers: + Cache-Control: ['no-cache, no-store, no-transform'] + Content-Security-Policy: [frame-ancestors 'self'] + Content-Type: [application/json;charset=UTF-8] + Date: ['Thu, 04 Apr 2019 11:39:00 GMT'] + Transfer-Encoding: [chunked] + Vary: [User-Agent] + X-AREQUESTID: [699x1204x1] + X-ASEN: [SEN-L13384799] + X-AUSERNAME: [anonymous] + X-Content-Type-Options: [nosniff] + X-Frame-Options: [SAMEORIGIN] + X-XSS-Protection: [1; mode=block] + content-length: ['4695'] + status: {code: 200, message: ''} +- request: + body: null + headers: + Accept: + - !!binary | + YXBwbGljYXRpb24vanNvbiwqLio7cT0wLjk= + Accept-Encoding: + - !!binary | + Z3ppcCwgZGVmbGF0ZQ== + Cache-Control: + - !!binary | + bm8tY2FjaGU= + Connection: + - !!binary | + a2VlcC1hbGl2ZQ== + Content-Type: + - !!binary | + YXBwbGljYXRpb24vanNvbg== + Cookie: + - !!binary | + YXRsYXNzaWFuLnhzcmYudG9rZW49QllHMy02U1BGLTBVTTEtMkxCT18yNDM2MWVkOGMxZTY3YjU1 + OWU4YTQ5MDA0NTg4MGE4YmZkZjM2MWExX2xvdXQ= + User-Agent: + - !!binary | + cHl0aG9uLXJlcXVlc3RzLzIuMjEuMA== + X-Atlassian-Token: + - !!binary | + bm8tY2hlY2s= + method: GET + uri: http://jira:8080/rest/api/2/myself + response: + body: {string: '{"message":"Client must be authenticated to access this resource.","status-code":401}'} + headers: + Cache-Control: [no-transform] + Content-Security-Policy: [frame-ancestors 'self'] + Content-Type: [application/json;charset=UTF-8] + Date: ['Thu, 04 Apr 2019 11:39:00 GMT'] + Transfer-Encoding: [chunked] + Vary: [User-Agent] + WWW-Authenticate: [OAuth realm="http%3A%2F%2Fjira%3A8080"] + X-AREQUESTID: [699x1205x1] + X-ASEN: [SEN-L13384799] + X-AUSERNAME: [anonymous] + X-Content-Type-Options: [nosniff] + X-Frame-Options: [SAMEORIGIN] + X-XSS-Protection: [1; mode=block] + content-length: ['85'] + status: {code: 401, message: ''} +- request: + body: null + headers: + Accept: + - !!binary | + YXBwbGljYXRpb24vanNvbiwqLio7cT0wLjk= + Accept-Encoding: + - !!binary | + Z3ppcCwgZGVmbGF0ZQ== + Cache-Control: + - !!binary | + bm8tY2FjaGU= + Connection: + - !!binary | + a2VlcC1hbGl2ZQ== + Content-Type: + - !!binary | + YXBwbGljYXRpb24vanNvbg== + Cookie: + - !!binary | + YXRsYXNzaWFuLnhzcmYudG9rZW49QllHMy02U1BGLTBVTTEtMkxCT18yNDM2MWVkOGMxZTY3YjU1 + OWU4YTQ5MDA0NTg4MGE4YmZkZjM2MWExX2xvdXQ= + User-Agent: + - !!binary | + cHl0aG9uLXJlcXVlc3RzLzIuMjEuMA== + X-Atlassian-Token: + - !!binary | + bm8tY2hlY2s= + method: GET + uri: http://jira:8080/rest/api/2/myself + response: + body: {string: '{"message":"Client must be authenticated to access this resource.","status-code":401}'} + headers: + Cache-Control: [no-transform] + Content-Security-Policy: [frame-ancestors 'self'] + Content-Type: [application/json;charset=UTF-8] + Date: ['Thu, 04 Apr 2019 11:39:09 GMT'] + Transfer-Encoding: [chunked] + Vary: [User-Agent] + WWW-Authenticate: [OAuth realm="http%3A%2F%2Fjira%3A8080"] + X-AREQUESTID: [699x1206x1] + X-ASEN: [SEN-L13384799] + X-AUSERNAME: [anonymous] + X-Content-Type-Options: [nosniff] + X-Frame-Options: [SAMEORIGIN] + X-XSS-Protection: [1; mode=block] + content-length: ['85'] + status: {code: 401, message: ''} +- request: + body: null + headers: + Accept: + - !!binary | + YXBwbGljYXRpb24vanNvbiwqLio7cT0wLjk= + Accept-Encoding: + - !!binary | + Z3ppcCwgZGVmbGF0ZQ== + Cache-Control: + - !!binary | + bm8tY2FjaGU= + Connection: + - !!binary | + a2VlcC1hbGl2ZQ== + Content-Type: + - !!binary | + YXBwbGljYXRpb24vanNvbg== + Cookie: + - !!binary | + YXRsYXNzaWFuLnhzcmYudG9rZW49QllHMy02U1BGLTBVTTEtMkxCT18yNDM2MWVkOGMxZTY3YjU1 + OWU4YTQ5MDA0NTg4MGE4YmZkZjM2MWExX2xvdXQ= + User-Agent: + - !!binary | + cHl0aG9uLXJlcXVlc3RzLzIuMjEuMA== + X-Atlassian-Token: + - !!binary | + bm8tY2hlY2s= + method: GET + uri: http://jira:8080/rest/api/2/myself + response: + body: {string: '{"message":"Client must be authenticated to access this resource.","status-code":401}'} + headers: + Cache-Control: [no-transform] + Content-Security-Policy: [frame-ancestors 'self'] + Content-Type: [application/json;charset=UTF-8] + Date: ['Thu, 04 Apr 2019 11:39:27 GMT'] + Transfer-Encoding: [chunked] + Vary: [User-Agent] + WWW-Authenticate: [OAuth realm="http%3A%2F%2Fjira%3A8080"] + X-AREQUESTID: [699x1207x1] + X-ASEN: [SEN-L13384799] + X-AUSERNAME: [anonymous] + X-Content-Type-Options: [nosniff] + X-Frame-Options: [SAMEORIGIN] + X-XSS-Protection: [1; mode=block] + content-length: ['85'] + status: {code: 401, message: ''} +- request: + body: null + headers: + Accept: + - !!binary | + YXBwbGljYXRpb24vanNvbiwqLio7cT0wLjk= + Accept-Encoding: + - !!binary | + Z3ppcCwgZGVmbGF0ZQ== + Cache-Control: + - !!binary | + bm8tY2FjaGU= + Connection: + - !!binary | + a2VlcC1hbGl2ZQ== + Content-Type: + - !!binary | + YXBwbGljYXRpb24vanNvbg== + Cookie: + - !!binary | + YXRsYXNzaWFuLnhzcmYudG9rZW49QllHMy02U1BGLTBVTTEtMkxCT18yNDM2MWVkOGMxZTY3YjU1 + OWU4YTQ5MDA0NTg4MGE4YmZkZjM2MWExX2xvdXQ= + User-Agent: + - !!binary | + cHl0aG9uLXJlcXVlc3RzLzIuMjEuMA== + X-Atlassian-Token: + - !!binary | + bm8tY2hlY2s= + method: GET + uri: http://jira:8080/rest/api/2/myself + response: + body: {string: '{"message":"Client must be authenticated to access this resource.","status-code":401}'} + headers: + Cache-Control: [no-transform] + Content-Security-Policy: [frame-ancestors 'self'] + Content-Type: [application/json;charset=UTF-8] + Date: ['Thu, 04 Apr 2019 11:39:28 GMT'] + Transfer-Encoding: [chunked] + Vary: [User-Agent] + WWW-Authenticate: [OAuth realm="http%3A%2F%2Fjira%3A8080"] + X-AREQUESTID: [699x1208x1] + X-ASEN: [SEN-L13384799] + X-AUSERNAME: [anonymous] + X-Content-Type-Options: [nosniff] + X-Frame-Options: [SAMEORIGIN] + X-XSS-Protection: [1; mode=block] + content-length: ['85'] + status: {code: 401, message: ''} +version: 1 diff --git a/connector_jira/tests/fixtures/cassettes/test_auth_oauth.yaml b/connector_jira/tests/fixtures/cassettes/test_auth_oauth.yaml new file mode 100644 index 000000000..9bc1d7dd0 --- /dev/null +++ b/connector_jira/tests/fixtures/cassettes/test_auth_oauth.yaml @@ -0,0 +1,76 @@ +interactions: +- request: + body: null + headers: + Accept: + - !!binary | + Ki8q + Accept-Encoding: + - !!binary | + Z3ppcCwgZGVmbGF0ZQ== + Connection: + - !!binary | + a2VlcC1hbGl2ZQ== + Content-Length: + - !!binary | + MA== + User-Agent: + - !!binary | + cHl0aG9uLXJlcXVlc3RzLzIuMTkuMQ== + method: POST + uri: http://jira:8080/plugins/servlet/oauth/request-token + response: + body: {string: oauth_token=ygdy5CS2FGTKnGHoX29TehoRQvb6T19X&oauth_token_secret=pwq9Qzc7iax0JtoQqZdLvPlv4ReECZGh} + headers: + Content-Security-Policy: [frame-ancestors 'self'] + Content-Type: [text/plain;charset=UTF-8] + Date: ['Wed, 03 Apr 2019 14:49:44 GMT'] + Set-Cookie: [atlassian.xsrf.token=BYG3-6SPF-0UM1-2LBO_391215749860b95636848223385ab73412134dc5_lout; + Path=/] + Vary: [User-Agent] + X-AREQUESTID: [889x732x1] + X-ASEN: [SEN-L13384799] + X-AUSERNAME: [anonymous] + X-Content-Type-Options: [nosniff] + X-Frame-Options: [SAMEORIGIN] + X-XSS-Protection: [1; mode=block] + content-length: ['96'] + status: {code: 200, message: ''} +- request: + body: null + headers: + Accept: + - !!binary | + Ki8q + Accept-Encoding: + - !!binary | + Z3ppcCwgZGVmbGF0ZQ== + Connection: + - !!binary | + a2VlcC1hbGl2ZQ== + Content-Length: + - !!binary | + MA== + User-Agent: + - !!binary | + cHl0aG9uLXJlcXVlc3RzLzIuMTkuMQ== + method: POST + uri: http://jira:8080/plugins/servlet/oauth/access-token + response: + body: {string: oauth_token=o7XglNpQdA3pwzGZw9r6WA2X2XZcjaaI&oauth_token_secret=pwq9Qzc7iax0JtoQqZdLvPlv4ReECZGh&oauth_expires_in=157680000&oauth_session_handle=WYSDJUpF0KTQjsX5vAq3vcWJtcfcNw8C&oauth_authorization_expires_in=160272000} + headers: + Content-Security-Policy: [frame-ancestors 'self'] + Content-Type: [text/plain;charset=UTF-8] + Date: ['Wed, 03 Apr 2019 14:51:49 GMT'] + Set-Cookie: [atlassian.xsrf.token=BYG3-6SPF-0UM1-2LBO_a09f0120c8cc6d03ffc01dce7a72b73f1cae63d2_lout; + Path=/] + Vary: [User-Agent] + X-AREQUESTID: [891x742x1] + X-ASEN: [SEN-L13384799] + X-AUSERNAME: [anonymous] + X-Content-Type-Options: [nosniff] + X-Frame-Options: [SAMEORIGIN] + X-XSS-Protection: [1; mode=block] + content-length: ['218'] + status: {code: 200, message: ''} +version: 1 diff --git a/connector_jira/tests/fixtures/cassettes/test_import_issue_type_batch.yaml b/connector_jira/tests/fixtures/cassettes/test_import_issue_type_batch.yaml new file mode 100644 index 000000000..9971615d5 --- /dev/null +++ b/connector_jira/tests/fixtures/cassettes/test_import_issue_type_batch.yaml @@ -0,0 +1,1766 @@ +interactions: +- request: + body: null + headers: + Accept: + - !!binary | + YXBwbGljYXRpb24vanNvbiwqLio7cT0wLjk= + Accept-Encoding: + - !!binary | + Z3ppcCwgZGVmbGF0ZQ== + Cache-Control: + - !!binary | + bm8tY2FjaGU= + Connection: + - !!binary | + a2VlcC1hbGl2ZQ== + Content-Type: + - !!binary | + YXBwbGljYXRpb24vanNvbg== + User-Agent: + - !!binary | + cHl0aG9uLXJlcXVlc3RzLzIuMjEuMA== + X-Atlassian-Token: + - !!binary | + bm8tY2hlY2s= + method: GET + uri: http://jira:8080/rest/api/2/serverInfo + response: + body: {string: '{"baseUrl":"http://localhost:8080","version":"7.12.3","versionNumbers":[7,12,3],"deploymentType":"Server","buildNumber":712004,"buildDate":"2018-10-12T00:00:00.000+0000","serverTime":"2019-04-04T08:47:50.944+0000","scmInfo":"5ef91d760d7124da5ebec5c16a948a4a807698df","serverTitle":"Jira"}'} + headers: + Cache-Control: ['no-cache, no-store, no-transform'] + Content-Security-Policy: [frame-ancestors 'self'] + Content-Type: [application/json;charset=UTF-8] + Date: ['Thu, 04 Apr 2019 08:47:50 GMT'] + Set-Cookie: [JSESSIONID=A284F9F3C946F4DE90B46F23A9D322E4; Path=/; HttpOnly, + atlassian.xsrf.token=BYG3-6SPF-0UM1-2LBO_3a9e497f41c4cbb37d12feff7f471568305cbff1_lin; + Path=/] + Transfer-Encoding: [chunked] + Vary: [User-Agent] + X-AREQUESTID: [527x865x1] + X-ASEN: [SEN-L13384799] + X-ASESSIONID: [nhuy89] + X-AUSERNAME: [gbaconnier] + X-Content-Type-Options: [nosniff] + X-Frame-Options: [SAMEORIGIN] + X-Seraph-LoginReason: [OK] + X-XSS-Protection: [1; mode=block] + content-length: ['288'] + status: {code: 200, message: ''} +- request: + body: null + headers: + Accept: + - !!binary | + YXBwbGljYXRpb24vanNvbiwqLio7cT0wLjk= + Accept-Encoding: + - !!binary | + Z3ppcCwgZGVmbGF0ZQ== + Cache-Control: + - !!binary | + bm8tY2FjaGU= + Connection: + - !!binary | + a2VlcC1hbGl2ZQ== + Content-Type: + - !!binary | + YXBwbGljYXRpb24vanNvbg== + Cookie: + - !!binary | + SlNFU1NJT05JRD1BMjg0RjlGM0M5NDZGNERFOTBCNDZGMjNBOUQzMjJFNDsgYXRsYXNzaWFuLnhz + cmYudG9rZW49QllHMy02U1BGLTBVTTEtMkxCT18zYTllNDk3ZjQxYzRjYmIzN2QxMmZlZmY3ZjQ3 + MTU2ODMwNWNiZmYxX2xpbg== + User-Agent: + - !!binary | + cHl0aG9uLXJlcXVlc3RzLzIuMjEuMA== + X-Atlassian-Token: + - !!binary | + bm8tY2hlY2s= + method: GET + uri: http://jira:8080/rest/api/2/field + response: + body: {string: "[{\"id\":\"issuetype\",\"name\":\"Issue Type\",\"custom\":false,\"\ + orderable\":true,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"\ + issuetype\",\"type\"],\"schema\":{\"type\":\"issuetype\",\"system\":\"issuetype\"\ + }},{\"id\":\"components\",\"name\":\"Component/s\",\"custom\":false,\"orderable\"\ + :true,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"component\"\ + ],\"schema\":{\"type\":\"array\",\"items\":\"component\",\"system\":\"components\"\ + }},{\"id\":\"issuekey\",\"name\":\"Key\",\"custom\":false,\"orderable\":false,\"\ + navigable\":true,\"searchable\":false,\"clauseNames\":[\"id\",\"issue\",\"\ + issuekey\",\"key\"]},{\"id\":\"timespent\",\"name\":\"Time Spent\",\"custom\"\ + :false,\"orderable\":false,\"navigable\":true,\"searchable\":false,\"clauseNames\"\ + :[\"timespent\"],\"schema\":{\"type\":\"number\",\"system\":\"timespent\"\ + }},{\"id\":\"timeoriginalestimate\",\"name\":\"Original Estimate\",\"custom\"\ + :false,\"orderable\":false,\"navigable\":true,\"searchable\":false,\"clauseNames\"\ + :[\"originalEstimate\",\"timeoriginalestimate\"],\"schema\":{\"type\":\"number\"\ + ,\"system\":\"timeoriginalestimate\"}},{\"id\":\"project\",\"name\":\"Project\"\ + ,\"custom\":false,\"orderable\":false,\"navigable\":true,\"searchable\":true,\"\ + clauseNames\":[\"project\"],\"schema\":{\"type\":\"project\",\"system\":\"\ + project\"}},{\"id\":\"description\",\"name\":\"Description\",\"custom\":false,\"\ + orderable\":true,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"\ + description\"],\"schema\":{\"type\":\"string\",\"system\":\"description\"\ + }},{\"id\":\"fixVersions\",\"name\":\"Fix Version/s\",\"custom\":false,\"\ + orderable\":true,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"\ + fixVersion\"],\"schema\":{\"type\":\"array\",\"items\":\"version\",\"system\"\ + :\"fixVersions\"}},{\"id\":\"aggregatetimespent\",\"name\":\"\u03A3 Time Spent\"\ + ,\"custom\":false,\"orderable\":false,\"navigable\":true,\"searchable\":false,\"\ + clauseNames\":[],\"schema\":{\"type\":\"number\",\"system\":\"aggregatetimespent\"\ + }},{\"id\":\"resolution\",\"name\":\"Resolution\",\"custom\":false,\"orderable\"\ + :true,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"resolution\"\ + ],\"schema\":{\"type\":\"resolution\",\"system\":\"resolution\"}},{\"id\"\ + :\"timetracking\",\"name\":\"Time Tracking\",\"custom\":false,\"orderable\"\ + :true,\"navigable\":false,\"searchable\":true,\"clauseNames\":[],\"schema\"\ + :{\"type\":\"timetracking\",\"system\":\"timetracking\"}},{\"id\":\"customfield_10104\"\ + ,\"name\":\"Epic Color\",\"custom\":true,\"orderable\":true,\"navigable\"\ + :true,\"searchable\":true,\"clauseNames\":[\"cf[10104]\",\"Epic Color\"],\"\ + schema\":{\"type\":\"string\",\"custom\":\"com.pyxis.greenhopper.jira:gh-epic-color\"\ + ,\"customId\":10104}},{\"id\":\"customfield_10105\",\"name\":\"Rank\",\"custom\"\ + :true,\"orderable\":true,\"navigable\":true,\"searchable\":true,\"clauseNames\"\ + :[\"cf[10105]\",\"Rank\"],\"schema\":{\"type\":\"any\",\"custom\":\"com.pyxis.greenhopper.jira:gh-lexo-rank\"\ + ,\"customId\":10105}},{\"id\":\"security\",\"name\":\"Security Level\",\"\ + custom\":false,\"orderable\":true,\"navigable\":true,\"searchable\":true,\"\ + clauseNames\":[\"level\"],\"schema\":{\"type\":\"securitylevel\",\"system\"\ + :\"security\"}},{\"id\":\"customfield_10106\",\"name\":\"Story Points\",\"\ + custom\":true,\"orderable\":true,\"navigable\":true,\"searchable\":true,\"\ + clauseNames\":[\"cf[10106]\",\"Story Points\"],\"schema\":{\"type\":\"number\"\ + ,\"custom\":\"com.atlassian.jira.plugin.system.customfieldtypes:float\",\"\ + customId\":10106}},{\"id\":\"attachment\",\"name\":\"Attachment\",\"custom\"\ + :false,\"orderable\":true,\"navigable\":false,\"searchable\":true,\"clauseNames\"\ + :[\"attachments\"],\"schema\":{\"type\":\"array\",\"items\":\"attachment\"\ + ,\"system\":\"attachment\"}},{\"id\":\"aggregatetimeestimate\",\"name\":\"\ + \u03A3 Remaining Estimate\",\"custom\":false,\"orderable\":false,\"navigable\"\ + :true,\"searchable\":false,\"clauseNames\":[],\"schema\":{\"type\":\"number\"\ + ,\"system\":\"aggregatetimeestimate\"}},{\"id\":\"resolutiondate\",\"name\"\ + :\"Resolved\",\"custom\":false,\"orderable\":false,\"navigable\":true,\"searchable\"\ + :true,\"clauseNames\":[\"resolutiondate\",\"resolved\"],\"schema\":{\"type\"\ + :\"datetime\",\"system\":\"resolutiondate\"}},{\"id\":\"workratio\",\"name\"\ + :\"Work Ratio\",\"custom\":false,\"orderable\":false,\"navigable\":true,\"\ + searchable\":true,\"clauseNames\":[\"workratio\"],\"schema\":{\"type\":\"\ + number\",\"system\":\"workratio\"}},{\"id\":\"summary\",\"name\":\"Summary\"\ + ,\"custom\":false,\"orderable\":true,\"navigable\":true,\"searchable\":true,\"\ + clauseNames\":[\"summary\"],\"schema\":{\"type\":\"string\",\"system\":\"\ + summary\"}},{\"id\":\"lastViewed\",\"name\":\"Last Viewed\",\"custom\":false,\"\ + orderable\":false,\"navigable\":true,\"searchable\":false,\"clauseNames\"\ + :[\"lastViewed\"],\"schema\":{\"type\":\"datetime\",\"system\":\"lastViewed\"\ + }},{\"id\":\"watches\",\"name\":\"Watchers\",\"custom\":false,\"orderable\"\ + :false,\"navigable\":true,\"searchable\":false,\"clauseNames\":[\"watchers\"\ + ],\"schema\":{\"type\":\"watches\",\"system\":\"watches\"}},{\"id\":\"creator\"\ + ,\"name\":\"Creator\",\"custom\":false,\"orderable\":false,\"navigable\":true,\"\ + searchable\":true,\"clauseNames\":[\"creator\"],\"schema\":{\"type\":\"user\"\ + ,\"system\":\"creator\"}},{\"id\":\"thumbnail\",\"name\":\"Images\",\"custom\"\ + :false,\"orderable\":false,\"navigable\":true,\"searchable\":false,\"clauseNames\"\ + :[]},{\"id\":\"subtasks\",\"name\":\"Sub-Tasks\",\"custom\":false,\"orderable\"\ + :false,\"navigable\":true,\"searchable\":false,\"clauseNames\":[\"subtasks\"\ + ],\"schema\":{\"type\":\"array\",\"items\":\"issuelinks\",\"system\":\"subtasks\"\ + }},{\"id\":\"created\",\"name\":\"Created\",\"custom\":false,\"orderable\"\ + :false,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"created\"\ + ,\"createdDate\"],\"schema\":{\"type\":\"datetime\",\"system\":\"created\"\ + }},{\"id\":\"reporter\",\"name\":\"Reporter\",\"custom\":false,\"orderable\"\ + :true,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"reporter\"\ + ],\"schema\":{\"type\":\"user\",\"system\":\"reporter\"}},{\"id\":\"aggregateprogress\"\ + ,\"name\":\"\u03A3 Progress\",\"custom\":false,\"orderable\":false,\"navigable\"\ + :true,\"searchable\":false,\"clauseNames\":[],\"schema\":{\"type\":\"progress\"\ + ,\"system\":\"aggregateprogress\"}},{\"id\":\"customfield_10000\",\"name\"\ + :\"Development\",\"custom\":true,\"orderable\":true,\"navigable\":true,\"\ + searchable\":true,\"clauseNames\":[\"cf[10000]\",\"Development\"],\"schema\"\ + :{\"type\":\"any\",\"custom\":\"com.atlassian.jira.plugins.jira-development-integration-plugin:devsummary\"\ + ,\"customId\":10000}},{\"id\":\"priority\",\"name\":\"Priority\",\"custom\"\ + :false,\"orderable\":true,\"navigable\":true,\"searchable\":true,\"clauseNames\"\ + :[\"priority\"],\"schema\":{\"type\":\"priority\",\"system\":\"priority\"\ + }},{\"id\":\"customfield_10100\",\"name\":\"Sprint\",\"custom\":true,\"orderable\"\ + :true,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"cf[10100]\"\ + ,\"Sprint\"],\"schema\":{\"type\":\"array\",\"items\":\"string\",\"custom\"\ + :\"com.pyxis.greenhopper.jira:gh-sprint\",\"customId\":10100}},{\"id\":\"\ + customfield_10101\",\"name\":\"Epic Link\",\"custom\":true,\"orderable\":true,\"\ + navigable\":true,\"searchable\":true,\"clauseNames\":[\"cf[10101]\",\"Epic\ + \ Link\"],\"schema\":{\"type\":\"any\",\"custom\":\"com.pyxis.greenhopper.jira:gh-epic-link\"\ + ,\"customId\":10101}},{\"id\":\"customfield_10102\",\"name\":\"Epic Status\"\ + ,\"custom\":true,\"orderable\":true,\"navigable\":true,\"searchable\":true,\"\ + clauseNames\":[\"cf[10102]\",\"Epic Status\"],\"schema\":{\"type\":\"option\"\ + ,\"custom\":\"com.pyxis.greenhopper.jira:gh-epic-status\",\"customId\":10102}},{\"\ + id\":\"labels\",\"name\":\"Labels\",\"custom\":false,\"orderable\":true,\"\ + navigable\":true,\"searchable\":true,\"clauseNames\":[\"labels\"],\"schema\"\ + :{\"type\":\"array\",\"items\":\"string\",\"system\":\"labels\"}},{\"id\"\ + :\"customfield_10103\",\"name\":\"Epic Name\",\"custom\":true,\"orderable\"\ + :true,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"cf[10103]\"\ + ,\"Epic Name\"],\"schema\":{\"type\":\"string\",\"custom\":\"com.pyxis.greenhopper.jira:gh-epic-label\"\ + ,\"customId\":10103}},{\"id\":\"environment\",\"name\":\"Environment\",\"\ + custom\":false,\"orderable\":true,\"navigable\":true,\"searchable\":true,\"\ + clauseNames\":[\"environment\"],\"schema\":{\"type\":\"string\",\"system\"\ + :\"environment\"}},{\"id\":\"timeestimate\",\"name\":\"Remaining Estimate\"\ + ,\"custom\":false,\"orderable\":false,\"navigable\":true,\"searchable\":false,\"\ + clauseNames\":[\"remainingEstimate\",\"timeestimate\"],\"schema\":{\"type\"\ + :\"number\",\"system\":\"timeestimate\"}},{\"id\":\"aggregatetimeoriginalestimate\"\ + ,\"name\":\"\u03A3 Original Estimate\",\"custom\":false,\"orderable\":false,\"\ + navigable\":true,\"searchable\":false,\"clauseNames\":[],\"schema\":{\"type\"\ + :\"number\",\"system\":\"aggregatetimeoriginalestimate\"}},{\"id\":\"versions\"\ + ,\"name\":\"Affects Version/s\",\"custom\":false,\"orderable\":true,\"navigable\"\ + :true,\"searchable\":true,\"clauseNames\":[\"affectedVersion\"],\"schema\"\ + :{\"type\":\"array\",\"items\":\"version\",\"system\":\"versions\"}},{\"id\"\ + :\"duedate\",\"name\":\"Due Date\",\"custom\":false,\"orderable\":true,\"\ + navigable\":true,\"searchable\":true,\"clauseNames\":[\"due\",\"duedate\"\ + ],\"schema\":{\"type\":\"date\",\"system\":\"duedate\"}},{\"id\":\"progress\"\ + ,\"name\":\"Progress\",\"custom\":false,\"orderable\":false,\"navigable\"\ + :true,\"searchable\":false,\"clauseNames\":[\"progress\"],\"schema\":{\"type\"\ + :\"progress\",\"system\":\"progress\"}},{\"id\":\"comment\",\"name\":\"Comment\"\ + ,\"custom\":false,\"orderable\":true,\"navigable\":false,\"searchable\":true,\"\ + clauseNames\":[\"comment\"],\"schema\":{\"type\":\"comments-page\",\"system\"\ + :\"comment\"}},{\"id\":\"votes\",\"name\":\"Votes\",\"custom\":false,\"orderable\"\ + :false,\"navigable\":true,\"searchable\":false,\"clauseNames\":[\"votes\"\ + ],\"schema\":{\"type\":\"votes\",\"system\":\"votes\"}},{\"id\":\"issuelinks\"\ + ,\"name\":\"Linked Issues\",\"custom\":false,\"orderable\":true,\"navigable\"\ + :true,\"searchable\":true,\"clauseNames\":[],\"schema\":{\"type\":\"array\"\ + ,\"items\":\"issuelinks\",\"system\":\"issuelinks\"}},{\"id\":\"worklog\"\ + ,\"name\":\"Log Work\",\"custom\":false,\"orderable\":true,\"navigable\":false,\"\ + searchable\":true,\"clauseNames\":[],\"schema\":{\"type\":\"array\",\"items\"\ + :\"worklog\",\"system\":\"worklog\"}},{\"id\":\"assignee\",\"name\":\"Assignee\"\ + ,\"custom\":false,\"orderable\":true,\"navigable\":true,\"searchable\":true,\"\ + clauseNames\":[\"assignee\"],\"schema\":{\"type\":\"user\",\"system\":\"assignee\"\ + }},{\"id\":\"updated\",\"name\":\"Updated\",\"custom\":false,\"orderable\"\ + :false,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"updated\"\ + ,\"updatedDate\"],\"schema\":{\"type\":\"datetime\",\"system\":\"updated\"\ + }},{\"id\":\"status\",\"name\":\"Status\",\"custom\":false,\"orderable\":false,\"\ + navigable\":true,\"searchable\":true,\"clauseNames\":[\"status\"],\"schema\"\ + :{\"type\":\"status\",\"system\":\"status\"}}]"} + headers: + Cache-Control: ['no-cache, no-store, no-transform'] + Content-Security-Policy: [frame-ancestors 'self'] + Content-Type: [application/json;charset=UTF-8] + Date: ['Thu, 04 Apr 2019 08:47:50 GMT'] + Set-Cookie: [JSESSIONID=6761E3CECDCBF4CB54F5BA1A68863929; Path=/; HttpOnly] + Transfer-Encoding: [chunked] + Vary: [User-Agent] + X-AREQUESTID: [527x866x1] + X-ASEN: [SEN-L13384799] + X-ASESSIONID: [5b6jp7] + X-AUSERNAME: [gbaconnier] + X-Content-Type-Options: [nosniff] + X-Frame-Options: [SAMEORIGIN] + X-Seraph-LoginReason: [OK] + X-XSS-Protection: [1; mode=block] + content-length: ['9557'] + status: {code: 200, message: ''} +- request: + body: null + headers: + Accept: + - !!binary | + YXBwbGljYXRpb24vanNvbiwqLio7cT0wLjk= + Accept-Encoding: + - !!binary | + Z3ppcCwgZGVmbGF0ZQ== + Cache-Control: + - !!binary | + bm8tY2FjaGU= + Connection: + - !!binary | + a2VlcC1hbGl2ZQ== + Content-Type: + - !!binary | + YXBwbGljYXRpb24vanNvbg== + Cookie: + - !!binary | + SlNFU1NJT05JRD02NzYxRTNDRUNEQ0JGNENCNTRGNUJBMUE2ODg2MzkyOTsgYXRsYXNzaWFuLnhz + cmYudG9rZW49QllHMy02U1BGLTBVTTEtMkxCT18zYTllNDk3ZjQxYzRjYmIzN2QxMmZlZmY3ZjQ3 + MTU2ODMwNWNiZmYxX2xpbg== + User-Agent: + - !!binary | + cHl0aG9uLXJlcXVlc3RzLzIuMjEuMA== + X-Atlassian-Token: + - !!binary | + bm8tY2hlY2s= + method: GET + uri: http://jira:8080/rest/api/2/issuetype + response: + body: {string: '[{"self":"http://jira:8080/rest/api/2/issuetype/10002","id":"10002","description":"A + task that needs to be done.","iconUrl":"http://jira:8080/secure/viewavatar?size=xsmall&avatarId=10318&avatarType=issuetype","name":"Task","subtask":false,"avatarId":10318},{"self":"http://jira:8080/rest/api/2/issuetype/10003","id":"10003","description":"The + sub-task of the issue","iconUrl":"http://jira:8080/secure/viewavatar?size=xsmall&avatarId=10316&avatarType=issuetype","name":"Sub-task","subtask":true,"avatarId":10316},{"self":"http://jira:8080/rest/api/2/issuetype/10001","id":"10001","description":"Created + by Jira Software - do not edit or delete. Issue type for a user story.","iconUrl":"http://jira:8080/images/icons/issuetypes/story.svg","name":"Story","subtask":false},{"self":"http://jira:8080/rest/api/2/issuetype/10004","id":"10004","description":"A + problem which impairs or prevents the functions of the product.","iconUrl":"http://jira:8080/secure/viewavatar?size=xsmall&avatarId=10303&avatarType=issuetype","name":"Bug","subtask":false,"avatarId":10303},{"self":"http://jira:8080/rest/api/2/issuetype/10000","id":"10000","description":"Created + by Jira Software - do not edit or delete. Issue type for a big user story + that needs to be broken down.","iconUrl":"http://jira:8080/images/icons/issuetypes/epic.svg","name":"Epic","subtask":false}]'} + headers: + Cache-Control: ['no-cache, no-store, no-transform'] + Content-Security-Policy: [frame-ancestors 'self'] + Content-Type: [application/json;charset=UTF-8] + Date: ['Thu, 04 Apr 2019 08:47:50 GMT'] + Set-Cookie: [JSESSIONID=09FE672783BC464570E54E584261DAAB; Path=/; HttpOnly] + Transfer-Encoding: [chunked] + Vary: [User-Agent] + X-AREQUESTID: [527x867x1] + X-ASEN: [SEN-L13384799] + X-ASESSIONID: [2pq75o] + X-AUSERNAME: [gbaconnier] + X-Content-Type-Options: [nosniff] + X-Frame-Options: [SAMEORIGIN] + X-Seraph-LoginReason: [OK] + X-XSS-Protection: [1; mode=block] + content-length: ['1348'] + status: {code: 200, message: ''} +- request: + body: null + headers: + Accept: + - !!binary | + YXBwbGljYXRpb24vanNvbiwqLio7cT0wLjk= + Accept-Encoding: + - !!binary | + Z3ppcCwgZGVmbGF0ZQ== + Cache-Control: + - !!binary | + bm8tY2FjaGU= + Connection: + - !!binary | + a2VlcC1hbGl2ZQ== + Content-Type: + - !!binary | + YXBwbGljYXRpb24vanNvbg== + User-Agent: + - !!binary | + cHl0aG9uLXJlcXVlc3RzLzIuMjEuMA== + X-Atlassian-Token: + - !!binary | + bm8tY2hlY2s= + method: GET + uri: http://jira:8080/rest/api/2/serverInfo + response: + body: {string: '{"baseUrl":"http://localhost:8080","version":"7.12.3","versionNumbers":[7,12,3],"deploymentType":"Server","buildNumber":712004,"buildDate":"2018-10-12T00:00:00.000+0000","serverTime":"2019-04-04T08:47:51.032+0000","scmInfo":"5ef91d760d7124da5ebec5c16a948a4a807698df","serverTitle":"Jira"}'} + headers: + Cache-Control: ['no-cache, no-store, no-transform'] + Content-Security-Policy: [frame-ancestors 'self'] + Content-Type: [application/json;charset=UTF-8] + Date: ['Thu, 04 Apr 2019 08:47:50 GMT'] + Set-Cookie: [JSESSIONID=8FBBA4B8926FF9A4281D349789C62AA1; Path=/; HttpOnly, + atlassian.xsrf.token=BYG3-6SPF-0UM1-2LBO_c2c24c965e11aa18ea0402e2a5477e8c26f723a9_lin; + Path=/] + Transfer-Encoding: [chunked] + Vary: [User-Agent] + X-AREQUESTID: [527x868x1] + X-ASEN: [SEN-L13384799] + X-ASESSIONID: [19oay4n] + X-AUSERNAME: [gbaconnier] + X-Content-Type-Options: [nosniff] + X-Frame-Options: [SAMEORIGIN] + X-Seraph-LoginReason: [OK] + X-XSS-Protection: [1; mode=block] + content-length: ['288'] + status: {code: 200, message: ''} +- request: + body: null + headers: + Accept: + - !!binary | + YXBwbGljYXRpb24vanNvbiwqLio7cT0wLjk= + Accept-Encoding: + - !!binary | + Z3ppcCwgZGVmbGF0ZQ== + Cache-Control: + - !!binary | + bm8tY2FjaGU= + Connection: + - !!binary | + a2VlcC1hbGl2ZQ== + Content-Type: + - !!binary | + YXBwbGljYXRpb24vanNvbg== + Cookie: + - !!binary | + SlNFU1NJT05JRD04RkJCQTRCODkyNkZGOUE0MjgxRDM0OTc4OUM2MkFBMTsgYXRsYXNzaWFuLnhz + cmYudG9rZW49QllHMy02U1BGLTBVTTEtMkxCT19jMmMyNGM5NjVlMTFhYTE4ZWEwNDAyZTJhNTQ3 + N2U4YzI2ZjcyM2E5X2xpbg== + User-Agent: + - !!binary | + cHl0aG9uLXJlcXVlc3RzLzIuMjEuMA== + X-Atlassian-Token: + - !!binary | + bm8tY2hlY2s= + method: GET + uri: http://jira:8080/rest/api/2/field + response: + body: {string: "[{\"id\":\"issuetype\",\"name\":\"Issue Type\",\"custom\":false,\"\ + orderable\":true,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"\ + issuetype\",\"type\"],\"schema\":{\"type\":\"issuetype\",\"system\":\"issuetype\"\ + }},{\"id\":\"components\",\"name\":\"Component/s\",\"custom\":false,\"orderable\"\ + :true,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"component\"\ + ],\"schema\":{\"type\":\"array\",\"items\":\"component\",\"system\":\"components\"\ + }},{\"id\":\"issuekey\",\"name\":\"Key\",\"custom\":false,\"orderable\":false,\"\ + navigable\":true,\"searchable\":false,\"clauseNames\":[\"id\",\"issue\",\"\ + issuekey\",\"key\"]},{\"id\":\"timespent\",\"name\":\"Time Spent\",\"custom\"\ + :false,\"orderable\":false,\"navigable\":true,\"searchable\":false,\"clauseNames\"\ + :[\"timespent\"],\"schema\":{\"type\":\"number\",\"system\":\"timespent\"\ + }},{\"id\":\"timeoriginalestimate\",\"name\":\"Original Estimate\",\"custom\"\ + :false,\"orderable\":false,\"navigable\":true,\"searchable\":false,\"clauseNames\"\ + :[\"originalEstimate\",\"timeoriginalestimate\"],\"schema\":{\"type\":\"number\"\ + ,\"system\":\"timeoriginalestimate\"}},{\"id\":\"project\",\"name\":\"Project\"\ + ,\"custom\":false,\"orderable\":false,\"navigable\":true,\"searchable\":true,\"\ + clauseNames\":[\"project\"],\"schema\":{\"type\":\"project\",\"system\":\"\ + project\"}},{\"id\":\"description\",\"name\":\"Description\",\"custom\":false,\"\ + orderable\":true,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"\ + description\"],\"schema\":{\"type\":\"string\",\"system\":\"description\"\ + }},{\"id\":\"fixVersions\",\"name\":\"Fix Version/s\",\"custom\":false,\"\ + orderable\":true,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"\ + fixVersion\"],\"schema\":{\"type\":\"array\",\"items\":\"version\",\"system\"\ + :\"fixVersions\"}},{\"id\":\"aggregatetimespent\",\"name\":\"\u03A3 Time Spent\"\ + ,\"custom\":false,\"orderable\":false,\"navigable\":true,\"searchable\":false,\"\ + clauseNames\":[],\"schema\":{\"type\":\"number\",\"system\":\"aggregatetimespent\"\ + }},{\"id\":\"resolution\",\"name\":\"Resolution\",\"custom\":false,\"orderable\"\ + :true,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"resolution\"\ + ],\"schema\":{\"type\":\"resolution\",\"system\":\"resolution\"}},{\"id\"\ + :\"timetracking\",\"name\":\"Time Tracking\",\"custom\":false,\"orderable\"\ + :true,\"navigable\":false,\"searchable\":true,\"clauseNames\":[],\"schema\"\ + :{\"type\":\"timetracking\",\"system\":\"timetracking\"}},{\"id\":\"customfield_10104\"\ + ,\"name\":\"Epic Color\",\"custom\":true,\"orderable\":true,\"navigable\"\ + :true,\"searchable\":true,\"clauseNames\":[\"cf[10104]\",\"Epic Color\"],\"\ + schema\":{\"type\":\"string\",\"custom\":\"com.pyxis.greenhopper.jira:gh-epic-color\"\ + ,\"customId\":10104}},{\"id\":\"customfield_10105\",\"name\":\"Rank\",\"custom\"\ + :true,\"orderable\":true,\"navigable\":true,\"searchable\":true,\"clauseNames\"\ + :[\"cf[10105]\",\"Rank\"],\"schema\":{\"type\":\"any\",\"custom\":\"com.pyxis.greenhopper.jira:gh-lexo-rank\"\ + ,\"customId\":10105}},{\"id\":\"security\",\"name\":\"Security Level\",\"\ + custom\":false,\"orderable\":true,\"navigable\":true,\"searchable\":true,\"\ + clauseNames\":[\"level\"],\"schema\":{\"type\":\"securitylevel\",\"system\"\ + :\"security\"}},{\"id\":\"customfield_10106\",\"name\":\"Story Points\",\"\ + custom\":true,\"orderable\":true,\"navigable\":true,\"searchable\":true,\"\ + clauseNames\":[\"cf[10106]\",\"Story Points\"],\"schema\":{\"type\":\"number\"\ + ,\"custom\":\"com.atlassian.jira.plugin.system.customfieldtypes:float\",\"\ + customId\":10106}},{\"id\":\"attachment\",\"name\":\"Attachment\",\"custom\"\ + :false,\"orderable\":true,\"navigable\":false,\"searchable\":true,\"clauseNames\"\ + :[\"attachments\"],\"schema\":{\"type\":\"array\",\"items\":\"attachment\"\ + ,\"system\":\"attachment\"}},{\"id\":\"aggregatetimeestimate\",\"name\":\"\ + \u03A3 Remaining Estimate\",\"custom\":false,\"orderable\":false,\"navigable\"\ + :true,\"searchable\":false,\"clauseNames\":[],\"schema\":{\"type\":\"number\"\ + ,\"system\":\"aggregatetimeestimate\"}},{\"id\":\"resolutiondate\",\"name\"\ + :\"Resolved\",\"custom\":false,\"orderable\":false,\"navigable\":true,\"searchable\"\ + :true,\"clauseNames\":[\"resolutiondate\",\"resolved\"],\"schema\":{\"type\"\ + :\"datetime\",\"system\":\"resolutiondate\"}},{\"id\":\"workratio\",\"name\"\ + :\"Work Ratio\",\"custom\":false,\"orderable\":false,\"navigable\":true,\"\ + searchable\":true,\"clauseNames\":[\"workratio\"],\"schema\":{\"type\":\"\ + number\",\"system\":\"workratio\"}},{\"id\":\"summary\",\"name\":\"Summary\"\ + ,\"custom\":false,\"orderable\":true,\"navigable\":true,\"searchable\":true,\"\ + clauseNames\":[\"summary\"],\"schema\":{\"type\":\"string\",\"system\":\"\ + summary\"}},{\"id\":\"lastViewed\",\"name\":\"Last Viewed\",\"custom\":false,\"\ + orderable\":false,\"navigable\":true,\"searchable\":false,\"clauseNames\"\ + :[\"lastViewed\"],\"schema\":{\"type\":\"datetime\",\"system\":\"lastViewed\"\ + }},{\"id\":\"watches\",\"name\":\"Watchers\",\"custom\":false,\"orderable\"\ + :false,\"navigable\":true,\"searchable\":false,\"clauseNames\":[\"watchers\"\ + ],\"schema\":{\"type\":\"watches\",\"system\":\"watches\"}},{\"id\":\"creator\"\ + ,\"name\":\"Creator\",\"custom\":false,\"orderable\":false,\"navigable\":true,\"\ + searchable\":true,\"clauseNames\":[\"creator\"],\"schema\":{\"type\":\"user\"\ + ,\"system\":\"creator\"}},{\"id\":\"thumbnail\",\"name\":\"Images\",\"custom\"\ + :false,\"orderable\":false,\"navigable\":true,\"searchable\":false,\"clauseNames\"\ + :[]},{\"id\":\"subtasks\",\"name\":\"Sub-Tasks\",\"custom\":false,\"orderable\"\ + :false,\"navigable\":true,\"searchable\":false,\"clauseNames\":[\"subtasks\"\ + ],\"schema\":{\"type\":\"array\",\"items\":\"issuelinks\",\"system\":\"subtasks\"\ + }},{\"id\":\"created\",\"name\":\"Created\",\"custom\":false,\"orderable\"\ + :false,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"created\"\ + ,\"createdDate\"],\"schema\":{\"type\":\"datetime\",\"system\":\"created\"\ + }},{\"id\":\"reporter\",\"name\":\"Reporter\",\"custom\":false,\"orderable\"\ + :true,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"reporter\"\ + ],\"schema\":{\"type\":\"user\",\"system\":\"reporter\"}},{\"id\":\"aggregateprogress\"\ + ,\"name\":\"\u03A3 Progress\",\"custom\":false,\"orderable\":false,\"navigable\"\ + :true,\"searchable\":false,\"clauseNames\":[],\"schema\":{\"type\":\"progress\"\ + ,\"system\":\"aggregateprogress\"}},{\"id\":\"customfield_10000\",\"name\"\ + :\"Development\",\"custom\":true,\"orderable\":true,\"navigable\":true,\"\ + searchable\":true,\"clauseNames\":[\"cf[10000]\",\"Development\"],\"schema\"\ + :{\"type\":\"any\",\"custom\":\"com.atlassian.jira.plugins.jira-development-integration-plugin:devsummary\"\ + ,\"customId\":10000}},{\"id\":\"priority\",\"name\":\"Priority\",\"custom\"\ + :false,\"orderable\":true,\"navigable\":true,\"searchable\":true,\"clauseNames\"\ + :[\"priority\"],\"schema\":{\"type\":\"priority\",\"system\":\"priority\"\ + }},{\"id\":\"customfield_10100\",\"name\":\"Sprint\",\"custom\":true,\"orderable\"\ + :true,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"cf[10100]\"\ + ,\"Sprint\"],\"schema\":{\"type\":\"array\",\"items\":\"string\",\"custom\"\ + :\"com.pyxis.greenhopper.jira:gh-sprint\",\"customId\":10100}},{\"id\":\"\ + customfield_10101\",\"name\":\"Epic Link\",\"custom\":true,\"orderable\":true,\"\ + navigable\":true,\"searchable\":true,\"clauseNames\":[\"cf[10101]\",\"Epic\ + \ Link\"],\"schema\":{\"type\":\"any\",\"custom\":\"com.pyxis.greenhopper.jira:gh-epic-link\"\ + ,\"customId\":10101}},{\"id\":\"customfield_10102\",\"name\":\"Epic Status\"\ + ,\"custom\":true,\"orderable\":true,\"navigable\":true,\"searchable\":true,\"\ + clauseNames\":[\"cf[10102]\",\"Epic Status\"],\"schema\":{\"type\":\"option\"\ + ,\"custom\":\"com.pyxis.greenhopper.jira:gh-epic-status\",\"customId\":10102}},{\"\ + id\":\"labels\",\"name\":\"Labels\",\"custom\":false,\"orderable\":true,\"\ + navigable\":true,\"searchable\":true,\"clauseNames\":[\"labels\"],\"schema\"\ + :{\"type\":\"array\",\"items\":\"string\",\"system\":\"labels\"}},{\"id\"\ + :\"customfield_10103\",\"name\":\"Epic Name\",\"custom\":true,\"orderable\"\ + :true,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"cf[10103]\"\ + ,\"Epic Name\"],\"schema\":{\"type\":\"string\",\"custom\":\"com.pyxis.greenhopper.jira:gh-epic-label\"\ + ,\"customId\":10103}},{\"id\":\"environment\",\"name\":\"Environment\",\"\ + custom\":false,\"orderable\":true,\"navigable\":true,\"searchable\":true,\"\ + clauseNames\":[\"environment\"],\"schema\":{\"type\":\"string\",\"system\"\ + :\"environment\"}},{\"id\":\"timeestimate\",\"name\":\"Remaining Estimate\"\ + ,\"custom\":false,\"orderable\":false,\"navigable\":true,\"searchable\":false,\"\ + clauseNames\":[\"remainingEstimate\",\"timeestimate\"],\"schema\":{\"type\"\ + :\"number\",\"system\":\"timeestimate\"}},{\"id\":\"aggregatetimeoriginalestimate\"\ + ,\"name\":\"\u03A3 Original Estimate\",\"custom\":false,\"orderable\":false,\"\ + navigable\":true,\"searchable\":false,\"clauseNames\":[],\"schema\":{\"type\"\ + :\"number\",\"system\":\"aggregatetimeoriginalestimate\"}},{\"id\":\"versions\"\ + ,\"name\":\"Affects Version/s\",\"custom\":false,\"orderable\":true,\"navigable\"\ + :true,\"searchable\":true,\"clauseNames\":[\"affectedVersion\"],\"schema\"\ + :{\"type\":\"array\",\"items\":\"version\",\"system\":\"versions\"}},{\"id\"\ + :\"duedate\",\"name\":\"Due Date\",\"custom\":false,\"orderable\":true,\"\ + navigable\":true,\"searchable\":true,\"clauseNames\":[\"due\",\"duedate\"\ + ],\"schema\":{\"type\":\"date\",\"system\":\"duedate\"}},{\"id\":\"progress\"\ + ,\"name\":\"Progress\",\"custom\":false,\"orderable\":false,\"navigable\"\ + :true,\"searchable\":false,\"clauseNames\":[\"progress\"],\"schema\":{\"type\"\ + :\"progress\",\"system\":\"progress\"}},{\"id\":\"comment\",\"name\":\"Comment\"\ + ,\"custom\":false,\"orderable\":true,\"navigable\":false,\"searchable\":true,\"\ + clauseNames\":[\"comment\"],\"schema\":{\"type\":\"comments-page\",\"system\"\ + :\"comment\"}},{\"id\":\"votes\",\"name\":\"Votes\",\"custom\":false,\"orderable\"\ + :false,\"navigable\":true,\"searchable\":false,\"clauseNames\":[\"votes\"\ + ],\"schema\":{\"type\":\"votes\",\"system\":\"votes\"}},{\"id\":\"issuelinks\"\ + ,\"name\":\"Linked Issues\",\"custom\":false,\"orderable\":true,\"navigable\"\ + :true,\"searchable\":true,\"clauseNames\":[],\"schema\":{\"type\":\"array\"\ + ,\"items\":\"issuelinks\",\"system\":\"issuelinks\"}},{\"id\":\"worklog\"\ + ,\"name\":\"Log Work\",\"custom\":false,\"orderable\":true,\"navigable\":false,\"\ + searchable\":true,\"clauseNames\":[],\"schema\":{\"type\":\"array\",\"items\"\ + :\"worklog\",\"system\":\"worklog\"}},{\"id\":\"assignee\",\"name\":\"Assignee\"\ + ,\"custom\":false,\"orderable\":true,\"navigable\":true,\"searchable\":true,\"\ + clauseNames\":[\"assignee\"],\"schema\":{\"type\":\"user\",\"system\":\"assignee\"\ + }},{\"id\":\"updated\",\"name\":\"Updated\",\"custom\":false,\"orderable\"\ + :false,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"updated\"\ + ,\"updatedDate\"],\"schema\":{\"type\":\"datetime\",\"system\":\"updated\"\ + }},{\"id\":\"status\",\"name\":\"Status\",\"custom\":false,\"orderable\":false,\"\ + navigable\":true,\"searchable\":true,\"clauseNames\":[\"status\"],\"schema\"\ + :{\"type\":\"status\",\"system\":\"status\"}}]"} + headers: + Cache-Control: ['no-cache, no-store, no-transform'] + Content-Security-Policy: [frame-ancestors 'self'] + Content-Type: [application/json;charset=UTF-8] + Date: ['Thu, 04 Apr 2019 08:47:50 GMT'] + Set-Cookie: [JSESSIONID=B39C784459149141BDC06785E4E4CA0B; Path=/; HttpOnly] + Transfer-Encoding: [chunked] + Vary: [User-Agent] + X-AREQUESTID: [527x869x1] + X-ASEN: [SEN-L13384799] + X-ASESSIONID: [apvf7q] + X-AUSERNAME: [gbaconnier] + X-Content-Type-Options: [nosniff] + X-Frame-Options: [SAMEORIGIN] + X-Seraph-LoginReason: [OK] + X-XSS-Protection: [1; mode=block] + content-length: ['9557'] + status: {code: 200, message: ''} +- request: + body: null + headers: + Accept: + - !!binary | + YXBwbGljYXRpb24vanNvbiwqLio7cT0wLjk= + Accept-Encoding: + - !!binary | + Z3ppcCwgZGVmbGF0ZQ== + Cache-Control: + - !!binary | + bm8tY2FjaGU= + Connection: + - !!binary | + a2VlcC1hbGl2ZQ== + Content-Type: + - !!binary | + YXBwbGljYXRpb24vanNvbg== + Cookie: + - !!binary | + SlNFU1NJT05JRD1CMzlDNzg0NDU5MTQ5MTQxQkRDMDY3ODVFNEU0Q0EwQjsgYXRsYXNzaWFuLnhz + cmYudG9rZW49QllHMy02U1BGLTBVTTEtMkxCT19jMmMyNGM5NjVlMTFhYTE4ZWEwNDAyZTJhNTQ3 + N2U4YzI2ZjcyM2E5X2xpbg== + User-Agent: + - !!binary | + cHl0aG9uLXJlcXVlc3RzLzIuMjEuMA== + X-Atlassian-Token: + - !!binary | + bm8tY2hlY2s= + method: GET + uri: http://jira:8080/rest/api/2/issuetype/10002 + response: + body: {string: '{"self":"http://jira:8080/rest/api/2/issuetype/10002","id":"10002","description":"A + task that needs to be done.","iconUrl":"http://jira:8080/secure/viewavatar?size=xsmall&avatarId=10318&avatarType=issuetype","name":"Task","subtask":false,"avatarId":10318}'} + headers: + Cache-Control: ['no-cache, no-store, no-transform'] + Content-Security-Policy: [frame-ancestors 'self'] + Content-Type: [application/json;charset=UTF-8] + Date: ['Thu, 04 Apr 2019 08:47:50 GMT'] + Set-Cookie: [JSESSIONID=6F0F6E51042053CFA951AD74C7B13578; Path=/; HttpOnly] + Transfer-Encoding: [chunked] + Vary: [User-Agent] + X-AREQUESTID: [527x870x1] + X-ASEN: [SEN-L13384799] + X-ASESSIONID: [yw9uvr] + X-AUSERNAME: [gbaconnier] + X-Content-Type-Options: [nosniff] + X-Frame-Options: [SAMEORIGIN] + X-Seraph-LoginReason: [OK] + X-XSS-Protection: [1; mode=block] + content-length: ['255'] + status: {code: 200, message: ''} +- request: + body: null + headers: + Accept: + - !!binary | + YXBwbGljYXRpb24vanNvbiwqLio7cT0wLjk= + Accept-Encoding: + - !!binary | + Z3ppcCwgZGVmbGF0ZQ== + Cache-Control: + - !!binary | + bm8tY2FjaGU= + Connection: + - !!binary | + a2VlcC1hbGl2ZQ== + Content-Type: + - !!binary | + YXBwbGljYXRpb24vanNvbg== + User-Agent: + - !!binary | + cHl0aG9uLXJlcXVlc3RzLzIuMjEuMA== + X-Atlassian-Token: + - !!binary | + bm8tY2hlY2s= + method: GET + uri: http://jira:8080/rest/api/2/serverInfo + response: + body: {string: '{"baseUrl":"http://localhost:8080","version":"7.12.3","versionNumbers":[7,12,3],"deploymentType":"Server","buildNumber":712004,"buildDate":"2018-10-12T00:00:00.000+0000","serverTime":"2019-04-04T08:47:51.124+0000","scmInfo":"5ef91d760d7124da5ebec5c16a948a4a807698df","serverTitle":"Jira"}'} + headers: + Cache-Control: ['no-cache, no-store, no-transform'] + Content-Security-Policy: [frame-ancestors 'self'] + Content-Type: [application/json;charset=UTF-8] + Date: ['Thu, 04 Apr 2019 08:47:50 GMT'] + Set-Cookie: [JSESSIONID=7D9C5AFDAC293751E952A422A64000C8; Path=/; HttpOnly, + atlassian.xsrf.token=BYG3-6SPF-0UM1-2LBO_37be6e80153b108551846cda18072cbee346b443_lin; + Path=/] + Transfer-Encoding: [chunked] + Vary: [User-Agent] + X-AREQUESTID: [527x871x1] + X-ASEN: [SEN-L13384799] + X-ASESSIONID: [187k70u] + X-AUSERNAME: [gbaconnier] + X-Content-Type-Options: [nosniff] + X-Frame-Options: [SAMEORIGIN] + X-Seraph-LoginReason: [OK] + X-XSS-Protection: [1; mode=block] + content-length: ['288'] + status: {code: 200, message: ''} +- request: + body: null + headers: + Accept: + - !!binary | + YXBwbGljYXRpb24vanNvbiwqLio7cT0wLjk= + Accept-Encoding: + - !!binary | + Z3ppcCwgZGVmbGF0ZQ== + Cache-Control: + - !!binary | + bm8tY2FjaGU= + Connection: + - !!binary | + a2VlcC1hbGl2ZQ== + Content-Type: + - !!binary | + YXBwbGljYXRpb24vanNvbg== + Cookie: + - !!binary | + SlNFU1NJT05JRD03RDlDNUFGREFDMjkzNzUxRTk1MkE0MjJBNjQwMDBDODsgYXRsYXNzaWFuLnhz + cmYudG9rZW49QllHMy02U1BGLTBVTTEtMkxCT18zN2JlNmU4MDE1M2IxMDg1NTE4NDZjZGExODA3 + MmNiZWUzNDZiNDQzX2xpbg== + User-Agent: + - !!binary | + cHl0aG9uLXJlcXVlc3RzLzIuMjEuMA== + X-Atlassian-Token: + - !!binary | + bm8tY2hlY2s= + method: GET + uri: http://jira:8080/rest/api/2/field + response: + body: {string: "[{\"id\":\"issuetype\",\"name\":\"Issue Type\",\"custom\":false,\"\ + orderable\":true,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"\ + issuetype\",\"type\"],\"schema\":{\"type\":\"issuetype\",\"system\":\"issuetype\"\ + }},{\"id\":\"components\",\"name\":\"Component/s\",\"custom\":false,\"orderable\"\ + :true,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"component\"\ + ],\"schema\":{\"type\":\"array\",\"items\":\"component\",\"system\":\"components\"\ + }},{\"id\":\"issuekey\",\"name\":\"Key\",\"custom\":false,\"orderable\":false,\"\ + navigable\":true,\"searchable\":false,\"clauseNames\":[\"id\",\"issue\",\"\ + issuekey\",\"key\"]},{\"id\":\"timespent\",\"name\":\"Time Spent\",\"custom\"\ + :false,\"orderable\":false,\"navigable\":true,\"searchable\":false,\"clauseNames\"\ + :[\"timespent\"],\"schema\":{\"type\":\"number\",\"system\":\"timespent\"\ + }},{\"id\":\"timeoriginalestimate\",\"name\":\"Original Estimate\",\"custom\"\ + :false,\"orderable\":false,\"navigable\":true,\"searchable\":false,\"clauseNames\"\ + :[\"originalEstimate\",\"timeoriginalestimate\"],\"schema\":{\"type\":\"number\"\ + ,\"system\":\"timeoriginalestimate\"}},{\"id\":\"project\",\"name\":\"Project\"\ + ,\"custom\":false,\"orderable\":false,\"navigable\":true,\"searchable\":true,\"\ + clauseNames\":[\"project\"],\"schema\":{\"type\":\"project\",\"system\":\"\ + project\"}},{\"id\":\"description\",\"name\":\"Description\",\"custom\":false,\"\ + orderable\":true,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"\ + description\"],\"schema\":{\"type\":\"string\",\"system\":\"description\"\ + }},{\"id\":\"fixVersions\",\"name\":\"Fix Version/s\",\"custom\":false,\"\ + orderable\":true,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"\ + fixVersion\"],\"schema\":{\"type\":\"array\",\"items\":\"version\",\"system\"\ + :\"fixVersions\"}},{\"id\":\"aggregatetimespent\",\"name\":\"\u03A3 Time Spent\"\ + ,\"custom\":false,\"orderable\":false,\"navigable\":true,\"searchable\":false,\"\ + clauseNames\":[],\"schema\":{\"type\":\"number\",\"system\":\"aggregatetimespent\"\ + }},{\"id\":\"resolution\",\"name\":\"Resolution\",\"custom\":false,\"orderable\"\ + :true,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"resolution\"\ + ],\"schema\":{\"type\":\"resolution\",\"system\":\"resolution\"}},{\"id\"\ + :\"timetracking\",\"name\":\"Time Tracking\",\"custom\":false,\"orderable\"\ + :true,\"navigable\":false,\"searchable\":true,\"clauseNames\":[],\"schema\"\ + :{\"type\":\"timetracking\",\"system\":\"timetracking\"}},{\"id\":\"customfield_10104\"\ + ,\"name\":\"Epic Color\",\"custom\":true,\"orderable\":true,\"navigable\"\ + :true,\"searchable\":true,\"clauseNames\":[\"cf[10104]\",\"Epic Color\"],\"\ + schema\":{\"type\":\"string\",\"custom\":\"com.pyxis.greenhopper.jira:gh-epic-color\"\ + ,\"customId\":10104}},{\"id\":\"customfield_10105\",\"name\":\"Rank\",\"custom\"\ + :true,\"orderable\":true,\"navigable\":true,\"searchable\":true,\"clauseNames\"\ + :[\"cf[10105]\",\"Rank\"],\"schema\":{\"type\":\"any\",\"custom\":\"com.pyxis.greenhopper.jira:gh-lexo-rank\"\ + ,\"customId\":10105}},{\"id\":\"security\",\"name\":\"Security Level\",\"\ + custom\":false,\"orderable\":true,\"navigable\":true,\"searchable\":true,\"\ + clauseNames\":[\"level\"],\"schema\":{\"type\":\"securitylevel\",\"system\"\ + :\"security\"}},{\"id\":\"customfield_10106\",\"name\":\"Story Points\",\"\ + custom\":true,\"orderable\":true,\"navigable\":true,\"searchable\":true,\"\ + clauseNames\":[\"cf[10106]\",\"Story Points\"],\"schema\":{\"type\":\"number\"\ + ,\"custom\":\"com.atlassian.jira.plugin.system.customfieldtypes:float\",\"\ + customId\":10106}},{\"id\":\"attachment\",\"name\":\"Attachment\",\"custom\"\ + :false,\"orderable\":true,\"navigable\":false,\"searchable\":true,\"clauseNames\"\ + :[\"attachments\"],\"schema\":{\"type\":\"array\",\"items\":\"attachment\"\ + ,\"system\":\"attachment\"}},{\"id\":\"aggregatetimeestimate\",\"name\":\"\ + \u03A3 Remaining Estimate\",\"custom\":false,\"orderable\":false,\"navigable\"\ + :true,\"searchable\":false,\"clauseNames\":[],\"schema\":{\"type\":\"number\"\ + ,\"system\":\"aggregatetimeestimate\"}},{\"id\":\"resolutiondate\",\"name\"\ + :\"Resolved\",\"custom\":false,\"orderable\":false,\"navigable\":true,\"searchable\"\ + :true,\"clauseNames\":[\"resolutiondate\",\"resolved\"],\"schema\":{\"type\"\ + :\"datetime\",\"system\":\"resolutiondate\"}},{\"id\":\"workratio\",\"name\"\ + :\"Work Ratio\",\"custom\":false,\"orderable\":false,\"navigable\":true,\"\ + searchable\":true,\"clauseNames\":[\"workratio\"],\"schema\":{\"type\":\"\ + number\",\"system\":\"workratio\"}},{\"id\":\"summary\",\"name\":\"Summary\"\ + ,\"custom\":false,\"orderable\":true,\"navigable\":true,\"searchable\":true,\"\ + clauseNames\":[\"summary\"],\"schema\":{\"type\":\"string\",\"system\":\"\ + summary\"}},{\"id\":\"lastViewed\",\"name\":\"Last Viewed\",\"custom\":false,\"\ + orderable\":false,\"navigable\":true,\"searchable\":false,\"clauseNames\"\ + :[\"lastViewed\"],\"schema\":{\"type\":\"datetime\",\"system\":\"lastViewed\"\ + }},{\"id\":\"watches\",\"name\":\"Watchers\",\"custom\":false,\"orderable\"\ + :false,\"navigable\":true,\"searchable\":false,\"clauseNames\":[\"watchers\"\ + ],\"schema\":{\"type\":\"watches\",\"system\":\"watches\"}},{\"id\":\"creator\"\ + ,\"name\":\"Creator\",\"custom\":false,\"orderable\":false,\"navigable\":true,\"\ + searchable\":true,\"clauseNames\":[\"creator\"],\"schema\":{\"type\":\"user\"\ + ,\"system\":\"creator\"}},{\"id\":\"thumbnail\",\"name\":\"Images\",\"custom\"\ + :false,\"orderable\":false,\"navigable\":true,\"searchable\":false,\"clauseNames\"\ + :[]},{\"id\":\"subtasks\",\"name\":\"Sub-Tasks\",\"custom\":false,\"orderable\"\ + :false,\"navigable\":true,\"searchable\":false,\"clauseNames\":[\"subtasks\"\ + ],\"schema\":{\"type\":\"array\",\"items\":\"issuelinks\",\"system\":\"subtasks\"\ + }},{\"id\":\"created\",\"name\":\"Created\",\"custom\":false,\"orderable\"\ + :false,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"created\"\ + ,\"createdDate\"],\"schema\":{\"type\":\"datetime\",\"system\":\"created\"\ + }},{\"id\":\"reporter\",\"name\":\"Reporter\",\"custom\":false,\"orderable\"\ + :true,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"reporter\"\ + ],\"schema\":{\"type\":\"user\",\"system\":\"reporter\"}},{\"id\":\"aggregateprogress\"\ + ,\"name\":\"\u03A3 Progress\",\"custom\":false,\"orderable\":false,\"navigable\"\ + :true,\"searchable\":false,\"clauseNames\":[],\"schema\":{\"type\":\"progress\"\ + ,\"system\":\"aggregateprogress\"}},{\"id\":\"customfield_10000\",\"name\"\ + :\"Development\",\"custom\":true,\"orderable\":true,\"navigable\":true,\"\ + searchable\":true,\"clauseNames\":[\"cf[10000]\",\"Development\"],\"schema\"\ + :{\"type\":\"any\",\"custom\":\"com.atlassian.jira.plugins.jira-development-integration-plugin:devsummary\"\ + ,\"customId\":10000}},{\"id\":\"priority\",\"name\":\"Priority\",\"custom\"\ + :false,\"orderable\":true,\"navigable\":true,\"searchable\":true,\"clauseNames\"\ + :[\"priority\"],\"schema\":{\"type\":\"priority\",\"system\":\"priority\"\ + }},{\"id\":\"customfield_10100\",\"name\":\"Sprint\",\"custom\":true,\"orderable\"\ + :true,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"cf[10100]\"\ + ,\"Sprint\"],\"schema\":{\"type\":\"array\",\"items\":\"string\",\"custom\"\ + :\"com.pyxis.greenhopper.jira:gh-sprint\",\"customId\":10100}},{\"id\":\"\ + customfield_10101\",\"name\":\"Epic Link\",\"custom\":true,\"orderable\":true,\"\ + navigable\":true,\"searchable\":true,\"clauseNames\":[\"cf[10101]\",\"Epic\ + \ Link\"],\"schema\":{\"type\":\"any\",\"custom\":\"com.pyxis.greenhopper.jira:gh-epic-link\"\ + ,\"customId\":10101}},{\"id\":\"customfield_10102\",\"name\":\"Epic Status\"\ + ,\"custom\":true,\"orderable\":true,\"navigable\":true,\"searchable\":true,\"\ + clauseNames\":[\"cf[10102]\",\"Epic Status\"],\"schema\":{\"type\":\"option\"\ + ,\"custom\":\"com.pyxis.greenhopper.jira:gh-epic-status\",\"customId\":10102}},{\"\ + id\":\"labels\",\"name\":\"Labels\",\"custom\":false,\"orderable\":true,\"\ + navigable\":true,\"searchable\":true,\"clauseNames\":[\"labels\"],\"schema\"\ + :{\"type\":\"array\",\"items\":\"string\",\"system\":\"labels\"}},{\"id\"\ + :\"customfield_10103\",\"name\":\"Epic Name\",\"custom\":true,\"orderable\"\ + :true,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"cf[10103]\"\ + ,\"Epic Name\"],\"schema\":{\"type\":\"string\",\"custom\":\"com.pyxis.greenhopper.jira:gh-epic-label\"\ + ,\"customId\":10103}},{\"id\":\"environment\",\"name\":\"Environment\",\"\ + custom\":false,\"orderable\":true,\"navigable\":true,\"searchable\":true,\"\ + clauseNames\":[\"environment\"],\"schema\":{\"type\":\"string\",\"system\"\ + :\"environment\"}},{\"id\":\"timeestimate\",\"name\":\"Remaining Estimate\"\ + ,\"custom\":false,\"orderable\":false,\"navigable\":true,\"searchable\":false,\"\ + clauseNames\":[\"remainingEstimate\",\"timeestimate\"],\"schema\":{\"type\"\ + :\"number\",\"system\":\"timeestimate\"}},{\"id\":\"aggregatetimeoriginalestimate\"\ + ,\"name\":\"\u03A3 Original Estimate\",\"custom\":false,\"orderable\":false,\"\ + navigable\":true,\"searchable\":false,\"clauseNames\":[],\"schema\":{\"type\"\ + :\"number\",\"system\":\"aggregatetimeoriginalestimate\"}},{\"id\":\"versions\"\ + ,\"name\":\"Affects Version/s\",\"custom\":false,\"orderable\":true,\"navigable\"\ + :true,\"searchable\":true,\"clauseNames\":[\"affectedVersion\"],\"schema\"\ + :{\"type\":\"array\",\"items\":\"version\",\"system\":\"versions\"}},{\"id\"\ + :\"duedate\",\"name\":\"Due Date\",\"custom\":false,\"orderable\":true,\"\ + navigable\":true,\"searchable\":true,\"clauseNames\":[\"due\",\"duedate\"\ + ],\"schema\":{\"type\":\"date\",\"system\":\"duedate\"}},{\"id\":\"progress\"\ + ,\"name\":\"Progress\",\"custom\":false,\"orderable\":false,\"navigable\"\ + :true,\"searchable\":false,\"clauseNames\":[\"progress\"],\"schema\":{\"type\"\ + :\"progress\",\"system\":\"progress\"}},{\"id\":\"comment\",\"name\":\"Comment\"\ + ,\"custom\":false,\"orderable\":true,\"navigable\":false,\"searchable\":true,\"\ + clauseNames\":[\"comment\"],\"schema\":{\"type\":\"comments-page\",\"system\"\ + :\"comment\"}},{\"id\":\"votes\",\"name\":\"Votes\",\"custom\":false,\"orderable\"\ + :false,\"navigable\":true,\"searchable\":false,\"clauseNames\":[\"votes\"\ + ],\"schema\":{\"type\":\"votes\",\"system\":\"votes\"}},{\"id\":\"issuelinks\"\ + ,\"name\":\"Linked Issues\",\"custom\":false,\"orderable\":true,\"navigable\"\ + :true,\"searchable\":true,\"clauseNames\":[],\"schema\":{\"type\":\"array\"\ + ,\"items\":\"issuelinks\",\"system\":\"issuelinks\"}},{\"id\":\"worklog\"\ + ,\"name\":\"Log Work\",\"custom\":false,\"orderable\":true,\"navigable\":false,\"\ + searchable\":true,\"clauseNames\":[],\"schema\":{\"type\":\"array\",\"items\"\ + :\"worklog\",\"system\":\"worklog\"}},{\"id\":\"assignee\",\"name\":\"Assignee\"\ + ,\"custom\":false,\"orderable\":true,\"navigable\":true,\"searchable\":true,\"\ + clauseNames\":[\"assignee\"],\"schema\":{\"type\":\"user\",\"system\":\"assignee\"\ + }},{\"id\":\"updated\",\"name\":\"Updated\",\"custom\":false,\"orderable\"\ + :false,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"updated\"\ + ,\"updatedDate\"],\"schema\":{\"type\":\"datetime\",\"system\":\"updated\"\ + }},{\"id\":\"status\",\"name\":\"Status\",\"custom\":false,\"orderable\":false,\"\ + navigable\":true,\"searchable\":true,\"clauseNames\":[\"status\"],\"schema\"\ + :{\"type\":\"status\",\"system\":\"status\"}}]"} + headers: + Cache-Control: ['no-cache, no-store, no-transform'] + Content-Security-Policy: [frame-ancestors 'self'] + Content-Type: [application/json;charset=UTF-8] + Date: ['Thu, 04 Apr 2019 08:47:50 GMT'] + Set-Cookie: [JSESSIONID=8B2DF9092966563AA3CD7796DC071B70; Path=/; HttpOnly] + Transfer-Encoding: [chunked] + Vary: [User-Agent] + X-AREQUESTID: [527x872x1] + X-ASEN: [SEN-L13384799] + X-ASESSIONID: [1lco607] + X-AUSERNAME: [gbaconnier] + X-Content-Type-Options: [nosniff] + X-Frame-Options: [SAMEORIGIN] + X-Seraph-LoginReason: [OK] + X-XSS-Protection: [1; mode=block] + content-length: ['9557'] + status: {code: 200, message: ''} +- request: + body: null + headers: + Accept: + - !!binary | + YXBwbGljYXRpb24vanNvbiwqLio7cT0wLjk= + Accept-Encoding: + - !!binary | + Z3ppcCwgZGVmbGF0ZQ== + Cache-Control: + - !!binary | + bm8tY2FjaGU= + Connection: + - !!binary | + a2VlcC1hbGl2ZQ== + Content-Type: + - !!binary | + YXBwbGljYXRpb24vanNvbg== + Cookie: + - !!binary | + SlNFU1NJT05JRD04QjJERjkwOTI5NjY1NjNBQTNDRDc3OTZEQzA3MUI3MDsgYXRsYXNzaWFuLnhz + cmYudG9rZW49QllHMy02U1BGLTBVTTEtMkxCT18zN2JlNmU4MDE1M2IxMDg1NTE4NDZjZGExODA3 + MmNiZWUzNDZiNDQzX2xpbg== + User-Agent: + - !!binary | + cHl0aG9uLXJlcXVlc3RzLzIuMjEuMA== + X-Atlassian-Token: + - !!binary | + bm8tY2hlY2s= + method: GET + uri: http://jira:8080/rest/api/2/issuetype/10003 + response: + body: {string: '{"self":"http://jira:8080/rest/api/2/issuetype/10003","id":"10003","description":"The + sub-task of the issue","iconUrl":"http://jira:8080/secure/viewavatar?size=xsmall&avatarId=10316&avatarType=issuetype","name":"Sub-task","subtask":true,"avatarId":10316}'} + headers: + Cache-Control: ['no-cache, no-store, no-transform'] + Content-Security-Policy: [frame-ancestors 'self'] + Content-Type: [application/json;charset=UTF-8] + Date: ['Thu, 04 Apr 2019 08:47:50 GMT'] + Set-Cookie: [JSESSIONID=4EB098AA2D926B6D3CD93F4E735A65DC; Path=/; HttpOnly] + Transfer-Encoding: [chunked] + Vary: [User-Agent] + X-AREQUESTID: [527x873x1] + X-ASEN: [SEN-L13384799] + X-ASESSIONID: [vjasnw] + X-AUSERNAME: [gbaconnier] + X-Content-Type-Options: [nosniff] + X-Frame-Options: [SAMEORIGIN] + X-Seraph-LoginReason: [OK] + X-XSS-Protection: [1; mode=block] + content-length: ['254'] + status: {code: 200, message: ''} +- request: + body: null + headers: + Accept: + - !!binary | + YXBwbGljYXRpb24vanNvbiwqLio7cT0wLjk= + Accept-Encoding: + - !!binary | + Z3ppcCwgZGVmbGF0ZQ== + Cache-Control: + - !!binary | + bm8tY2FjaGU= + Connection: + - !!binary | + a2VlcC1hbGl2ZQ== + Content-Type: + - !!binary | + YXBwbGljYXRpb24vanNvbg== + User-Agent: + - !!binary | + cHl0aG9uLXJlcXVlc3RzLzIuMjEuMA== + X-Atlassian-Token: + - !!binary | + bm8tY2hlY2s= + method: GET + uri: http://jira:8080/rest/api/2/serverInfo + response: + body: {string: '{"baseUrl":"http://localhost:8080","version":"7.12.3","versionNumbers":[7,12,3],"deploymentType":"Server","buildNumber":712004,"buildDate":"2018-10-12T00:00:00.000+0000","serverTime":"2019-04-04T08:47:51.203+0000","scmInfo":"5ef91d760d7124da5ebec5c16a948a4a807698df","serverTitle":"Jira"}'} + headers: + Cache-Control: ['no-cache, no-store, no-transform'] + Content-Security-Policy: [frame-ancestors 'self'] + Content-Type: [application/json;charset=UTF-8] + Date: ['Thu, 04 Apr 2019 08:47:50 GMT'] + Set-Cookie: [JSESSIONID=BF85A8C3CD4EAEB937336E33BF7C94A0; Path=/; HttpOnly, + atlassian.xsrf.token=BYG3-6SPF-0UM1-2LBO_3393f556ff4f62b6731dbaadc6375383c9f2e44b_lin; + Path=/] + Transfer-Encoding: [chunked] + Vary: [User-Agent] + X-AREQUESTID: [527x874x1] + X-ASEN: [SEN-L13384799] + X-ASESSIONID: [njv9cs] + X-AUSERNAME: [gbaconnier] + X-Content-Type-Options: [nosniff] + X-Frame-Options: [SAMEORIGIN] + X-Seraph-LoginReason: [OK] + X-XSS-Protection: [1; mode=block] + content-length: ['288'] + status: {code: 200, message: ''} +- request: + body: null + headers: + Accept: + - !!binary | + YXBwbGljYXRpb24vanNvbiwqLio7cT0wLjk= + Accept-Encoding: + - !!binary | + Z3ppcCwgZGVmbGF0ZQ== + Cache-Control: + - !!binary | + bm8tY2FjaGU= + Connection: + - !!binary | + a2VlcC1hbGl2ZQ== + Content-Type: + - !!binary | + YXBwbGljYXRpb24vanNvbg== + Cookie: + - !!binary | + SlNFU1NJT05JRD1CRjg1QThDM0NENEVBRUI5MzczMzZFMzNCRjdDOTRBMDsgYXRsYXNzaWFuLnhz + cmYudG9rZW49QllHMy02U1BGLTBVTTEtMkxCT18zMzkzZjU1NmZmNGY2MmI2NzMxZGJhYWRjNjM3 + NTM4M2M5ZjJlNDRiX2xpbg== + User-Agent: + - !!binary | + cHl0aG9uLXJlcXVlc3RzLzIuMjEuMA== + X-Atlassian-Token: + - !!binary | + bm8tY2hlY2s= + method: GET + uri: http://jira:8080/rest/api/2/field + response: + body: {string: "[{\"id\":\"issuetype\",\"name\":\"Issue Type\",\"custom\":false,\"\ + orderable\":true,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"\ + issuetype\",\"type\"],\"schema\":{\"type\":\"issuetype\",\"system\":\"issuetype\"\ + }},{\"id\":\"components\",\"name\":\"Component/s\",\"custom\":false,\"orderable\"\ + :true,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"component\"\ + ],\"schema\":{\"type\":\"array\",\"items\":\"component\",\"system\":\"components\"\ + }},{\"id\":\"issuekey\",\"name\":\"Key\",\"custom\":false,\"orderable\":false,\"\ + navigable\":true,\"searchable\":false,\"clauseNames\":[\"id\",\"issue\",\"\ + issuekey\",\"key\"]},{\"id\":\"timespent\",\"name\":\"Time Spent\",\"custom\"\ + :false,\"orderable\":false,\"navigable\":true,\"searchable\":false,\"clauseNames\"\ + :[\"timespent\"],\"schema\":{\"type\":\"number\",\"system\":\"timespent\"\ + }},{\"id\":\"timeoriginalestimate\",\"name\":\"Original Estimate\",\"custom\"\ + :false,\"orderable\":false,\"navigable\":true,\"searchable\":false,\"clauseNames\"\ + :[\"originalEstimate\",\"timeoriginalestimate\"],\"schema\":{\"type\":\"number\"\ + ,\"system\":\"timeoriginalestimate\"}},{\"id\":\"project\",\"name\":\"Project\"\ + ,\"custom\":false,\"orderable\":false,\"navigable\":true,\"searchable\":true,\"\ + clauseNames\":[\"project\"],\"schema\":{\"type\":\"project\",\"system\":\"\ + project\"}},{\"id\":\"description\",\"name\":\"Description\",\"custom\":false,\"\ + orderable\":true,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"\ + description\"],\"schema\":{\"type\":\"string\",\"system\":\"description\"\ + }},{\"id\":\"fixVersions\",\"name\":\"Fix Version/s\",\"custom\":false,\"\ + orderable\":true,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"\ + fixVersion\"],\"schema\":{\"type\":\"array\",\"items\":\"version\",\"system\"\ + :\"fixVersions\"}},{\"id\":\"aggregatetimespent\",\"name\":\"\u03A3 Time Spent\"\ + ,\"custom\":false,\"orderable\":false,\"navigable\":true,\"searchable\":false,\"\ + clauseNames\":[],\"schema\":{\"type\":\"number\",\"system\":\"aggregatetimespent\"\ + }},{\"id\":\"resolution\",\"name\":\"Resolution\",\"custom\":false,\"orderable\"\ + :true,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"resolution\"\ + ],\"schema\":{\"type\":\"resolution\",\"system\":\"resolution\"}},{\"id\"\ + :\"timetracking\",\"name\":\"Time Tracking\",\"custom\":false,\"orderable\"\ + :true,\"navigable\":false,\"searchable\":true,\"clauseNames\":[],\"schema\"\ + :{\"type\":\"timetracking\",\"system\":\"timetracking\"}},{\"id\":\"customfield_10104\"\ + ,\"name\":\"Epic Color\",\"custom\":true,\"orderable\":true,\"navigable\"\ + :true,\"searchable\":true,\"clauseNames\":[\"cf[10104]\",\"Epic Color\"],\"\ + schema\":{\"type\":\"string\",\"custom\":\"com.pyxis.greenhopper.jira:gh-epic-color\"\ + ,\"customId\":10104}},{\"id\":\"customfield_10105\",\"name\":\"Rank\",\"custom\"\ + :true,\"orderable\":true,\"navigable\":true,\"searchable\":true,\"clauseNames\"\ + :[\"cf[10105]\",\"Rank\"],\"schema\":{\"type\":\"any\",\"custom\":\"com.pyxis.greenhopper.jira:gh-lexo-rank\"\ + ,\"customId\":10105}},{\"id\":\"security\",\"name\":\"Security Level\",\"\ + custom\":false,\"orderable\":true,\"navigable\":true,\"searchable\":true,\"\ + clauseNames\":[\"level\"],\"schema\":{\"type\":\"securitylevel\",\"system\"\ + :\"security\"}},{\"id\":\"customfield_10106\",\"name\":\"Story Points\",\"\ + custom\":true,\"orderable\":true,\"navigable\":true,\"searchable\":true,\"\ + clauseNames\":[\"cf[10106]\",\"Story Points\"],\"schema\":{\"type\":\"number\"\ + ,\"custom\":\"com.atlassian.jira.plugin.system.customfieldtypes:float\",\"\ + customId\":10106}},{\"id\":\"attachment\",\"name\":\"Attachment\",\"custom\"\ + :false,\"orderable\":true,\"navigable\":false,\"searchable\":true,\"clauseNames\"\ + :[\"attachments\"],\"schema\":{\"type\":\"array\",\"items\":\"attachment\"\ + ,\"system\":\"attachment\"}},{\"id\":\"aggregatetimeestimate\",\"name\":\"\ + \u03A3 Remaining Estimate\",\"custom\":false,\"orderable\":false,\"navigable\"\ + :true,\"searchable\":false,\"clauseNames\":[],\"schema\":{\"type\":\"number\"\ + ,\"system\":\"aggregatetimeestimate\"}},{\"id\":\"resolutiondate\",\"name\"\ + :\"Resolved\",\"custom\":false,\"orderable\":false,\"navigable\":true,\"searchable\"\ + :true,\"clauseNames\":[\"resolutiondate\",\"resolved\"],\"schema\":{\"type\"\ + :\"datetime\",\"system\":\"resolutiondate\"}},{\"id\":\"workratio\",\"name\"\ + :\"Work Ratio\",\"custom\":false,\"orderable\":false,\"navigable\":true,\"\ + searchable\":true,\"clauseNames\":[\"workratio\"],\"schema\":{\"type\":\"\ + number\",\"system\":\"workratio\"}},{\"id\":\"summary\",\"name\":\"Summary\"\ + ,\"custom\":false,\"orderable\":true,\"navigable\":true,\"searchable\":true,\"\ + clauseNames\":[\"summary\"],\"schema\":{\"type\":\"string\",\"system\":\"\ + summary\"}},{\"id\":\"lastViewed\",\"name\":\"Last Viewed\",\"custom\":false,\"\ + orderable\":false,\"navigable\":true,\"searchable\":false,\"clauseNames\"\ + :[\"lastViewed\"],\"schema\":{\"type\":\"datetime\",\"system\":\"lastViewed\"\ + }},{\"id\":\"watches\",\"name\":\"Watchers\",\"custom\":false,\"orderable\"\ + :false,\"navigable\":true,\"searchable\":false,\"clauseNames\":[\"watchers\"\ + ],\"schema\":{\"type\":\"watches\",\"system\":\"watches\"}},{\"id\":\"creator\"\ + ,\"name\":\"Creator\",\"custom\":false,\"orderable\":false,\"navigable\":true,\"\ + searchable\":true,\"clauseNames\":[\"creator\"],\"schema\":{\"type\":\"user\"\ + ,\"system\":\"creator\"}},{\"id\":\"thumbnail\",\"name\":\"Images\",\"custom\"\ + :false,\"orderable\":false,\"navigable\":true,\"searchable\":false,\"clauseNames\"\ + :[]},{\"id\":\"subtasks\",\"name\":\"Sub-Tasks\",\"custom\":false,\"orderable\"\ + :false,\"navigable\":true,\"searchable\":false,\"clauseNames\":[\"subtasks\"\ + ],\"schema\":{\"type\":\"array\",\"items\":\"issuelinks\",\"system\":\"subtasks\"\ + }},{\"id\":\"created\",\"name\":\"Created\",\"custom\":false,\"orderable\"\ + :false,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"created\"\ + ,\"createdDate\"],\"schema\":{\"type\":\"datetime\",\"system\":\"created\"\ + }},{\"id\":\"reporter\",\"name\":\"Reporter\",\"custom\":false,\"orderable\"\ + :true,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"reporter\"\ + ],\"schema\":{\"type\":\"user\",\"system\":\"reporter\"}},{\"id\":\"aggregateprogress\"\ + ,\"name\":\"\u03A3 Progress\",\"custom\":false,\"orderable\":false,\"navigable\"\ + :true,\"searchable\":false,\"clauseNames\":[],\"schema\":{\"type\":\"progress\"\ + ,\"system\":\"aggregateprogress\"}},{\"id\":\"customfield_10000\",\"name\"\ + :\"Development\",\"custom\":true,\"orderable\":true,\"navigable\":true,\"\ + searchable\":true,\"clauseNames\":[\"cf[10000]\",\"Development\"],\"schema\"\ + :{\"type\":\"any\",\"custom\":\"com.atlassian.jira.plugins.jira-development-integration-plugin:devsummary\"\ + ,\"customId\":10000}},{\"id\":\"priority\",\"name\":\"Priority\",\"custom\"\ + :false,\"orderable\":true,\"navigable\":true,\"searchable\":true,\"clauseNames\"\ + :[\"priority\"],\"schema\":{\"type\":\"priority\",\"system\":\"priority\"\ + }},{\"id\":\"customfield_10100\",\"name\":\"Sprint\",\"custom\":true,\"orderable\"\ + :true,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"cf[10100]\"\ + ,\"Sprint\"],\"schema\":{\"type\":\"array\",\"items\":\"string\",\"custom\"\ + :\"com.pyxis.greenhopper.jira:gh-sprint\",\"customId\":10100}},{\"id\":\"\ + customfield_10101\",\"name\":\"Epic Link\",\"custom\":true,\"orderable\":true,\"\ + navigable\":true,\"searchable\":true,\"clauseNames\":[\"cf[10101]\",\"Epic\ + \ Link\"],\"schema\":{\"type\":\"any\",\"custom\":\"com.pyxis.greenhopper.jira:gh-epic-link\"\ + ,\"customId\":10101}},{\"id\":\"customfield_10102\",\"name\":\"Epic Status\"\ + ,\"custom\":true,\"orderable\":true,\"navigable\":true,\"searchable\":true,\"\ + clauseNames\":[\"cf[10102]\",\"Epic Status\"],\"schema\":{\"type\":\"option\"\ + ,\"custom\":\"com.pyxis.greenhopper.jira:gh-epic-status\",\"customId\":10102}},{\"\ + id\":\"labels\",\"name\":\"Labels\",\"custom\":false,\"orderable\":true,\"\ + navigable\":true,\"searchable\":true,\"clauseNames\":[\"labels\"],\"schema\"\ + :{\"type\":\"array\",\"items\":\"string\",\"system\":\"labels\"}},{\"id\"\ + :\"customfield_10103\",\"name\":\"Epic Name\",\"custom\":true,\"orderable\"\ + :true,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"cf[10103]\"\ + ,\"Epic Name\"],\"schema\":{\"type\":\"string\",\"custom\":\"com.pyxis.greenhopper.jira:gh-epic-label\"\ + ,\"customId\":10103}},{\"id\":\"environment\",\"name\":\"Environment\",\"\ + custom\":false,\"orderable\":true,\"navigable\":true,\"searchable\":true,\"\ + clauseNames\":[\"environment\"],\"schema\":{\"type\":\"string\",\"system\"\ + :\"environment\"}},{\"id\":\"timeestimate\",\"name\":\"Remaining Estimate\"\ + ,\"custom\":false,\"orderable\":false,\"navigable\":true,\"searchable\":false,\"\ + clauseNames\":[\"remainingEstimate\",\"timeestimate\"],\"schema\":{\"type\"\ + :\"number\",\"system\":\"timeestimate\"}},{\"id\":\"aggregatetimeoriginalestimate\"\ + ,\"name\":\"\u03A3 Original Estimate\",\"custom\":false,\"orderable\":false,\"\ + navigable\":true,\"searchable\":false,\"clauseNames\":[],\"schema\":{\"type\"\ + :\"number\",\"system\":\"aggregatetimeoriginalestimate\"}},{\"id\":\"versions\"\ + ,\"name\":\"Affects Version/s\",\"custom\":false,\"orderable\":true,\"navigable\"\ + :true,\"searchable\":true,\"clauseNames\":[\"affectedVersion\"],\"schema\"\ + :{\"type\":\"array\",\"items\":\"version\",\"system\":\"versions\"}},{\"id\"\ + :\"duedate\",\"name\":\"Due Date\",\"custom\":false,\"orderable\":true,\"\ + navigable\":true,\"searchable\":true,\"clauseNames\":[\"due\",\"duedate\"\ + ],\"schema\":{\"type\":\"date\",\"system\":\"duedate\"}},{\"id\":\"progress\"\ + ,\"name\":\"Progress\",\"custom\":false,\"orderable\":false,\"navigable\"\ + :true,\"searchable\":false,\"clauseNames\":[\"progress\"],\"schema\":{\"type\"\ + :\"progress\",\"system\":\"progress\"}},{\"id\":\"comment\",\"name\":\"Comment\"\ + ,\"custom\":false,\"orderable\":true,\"navigable\":false,\"searchable\":true,\"\ + clauseNames\":[\"comment\"],\"schema\":{\"type\":\"comments-page\",\"system\"\ + :\"comment\"}},{\"id\":\"votes\",\"name\":\"Votes\",\"custom\":false,\"orderable\"\ + :false,\"navigable\":true,\"searchable\":false,\"clauseNames\":[\"votes\"\ + ],\"schema\":{\"type\":\"votes\",\"system\":\"votes\"}},{\"id\":\"issuelinks\"\ + ,\"name\":\"Linked Issues\",\"custom\":false,\"orderable\":true,\"navigable\"\ + :true,\"searchable\":true,\"clauseNames\":[],\"schema\":{\"type\":\"array\"\ + ,\"items\":\"issuelinks\",\"system\":\"issuelinks\"}},{\"id\":\"worklog\"\ + ,\"name\":\"Log Work\",\"custom\":false,\"orderable\":true,\"navigable\":false,\"\ + searchable\":true,\"clauseNames\":[],\"schema\":{\"type\":\"array\",\"items\"\ + :\"worklog\",\"system\":\"worklog\"}},{\"id\":\"assignee\",\"name\":\"Assignee\"\ + ,\"custom\":false,\"orderable\":true,\"navigable\":true,\"searchable\":true,\"\ + clauseNames\":[\"assignee\"],\"schema\":{\"type\":\"user\",\"system\":\"assignee\"\ + }},{\"id\":\"updated\",\"name\":\"Updated\",\"custom\":false,\"orderable\"\ + :false,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"updated\"\ + ,\"updatedDate\"],\"schema\":{\"type\":\"datetime\",\"system\":\"updated\"\ + }},{\"id\":\"status\",\"name\":\"Status\",\"custom\":false,\"orderable\":false,\"\ + navigable\":true,\"searchable\":true,\"clauseNames\":[\"status\"],\"schema\"\ + :{\"type\":\"status\",\"system\":\"status\"}}]"} + headers: + Cache-Control: ['no-cache, no-store, no-transform'] + Content-Security-Policy: [frame-ancestors 'self'] + Content-Type: [application/json;charset=UTF-8] + Date: ['Thu, 04 Apr 2019 08:47:50 GMT'] + Set-Cookie: [JSESSIONID=944AB3971637AA62CD2D1BC08F20525A; Path=/; HttpOnly] + Transfer-Encoding: [chunked] + Vary: [User-Agent] + X-AREQUESTID: [527x875x1] + X-ASEN: [SEN-L13384799] + X-ASESSIONID: [11lkqre] + X-AUSERNAME: [gbaconnier] + X-Content-Type-Options: [nosniff] + X-Frame-Options: [SAMEORIGIN] + X-Seraph-LoginReason: [OK] + X-XSS-Protection: [1; mode=block] + content-length: ['9557'] + status: {code: 200, message: ''} +- request: + body: null + headers: + Accept: + - !!binary | + YXBwbGljYXRpb24vanNvbiwqLio7cT0wLjk= + Accept-Encoding: + - !!binary | + Z3ppcCwgZGVmbGF0ZQ== + Cache-Control: + - !!binary | + bm8tY2FjaGU= + Connection: + - !!binary | + a2VlcC1hbGl2ZQ== + Content-Type: + - !!binary | + YXBwbGljYXRpb24vanNvbg== + Cookie: + - !!binary | + SlNFU1NJT05JRD05NDRBQjM5NzE2MzdBQTYyQ0QyRDFCQzA4RjIwNTI1QTsgYXRsYXNzaWFuLnhz + cmYudG9rZW49QllHMy02U1BGLTBVTTEtMkxCT18zMzkzZjU1NmZmNGY2MmI2NzMxZGJhYWRjNjM3 + NTM4M2M5ZjJlNDRiX2xpbg== + User-Agent: + - !!binary | + cHl0aG9uLXJlcXVlc3RzLzIuMjEuMA== + X-Atlassian-Token: + - !!binary | + bm8tY2hlY2s= + method: GET + uri: http://jira:8080/rest/api/2/issuetype/10001 + response: + body: {string: '{"self":"http://jira:8080/rest/api/2/issuetype/10001","id":"10001","description":"Created + by Jira Software - do not edit or delete. Issue type for a user story.","iconUrl":"http://jira:8080/images/icons/issuetypes/story.svg","name":"Story","subtask":false}'} + headers: + Cache-Control: ['no-cache, no-store, no-transform'] + Content-Security-Policy: [frame-ancestors 'self'] + Content-Type: [application/json;charset=UTF-8] + Date: ['Thu, 04 Apr 2019 08:47:50 GMT'] + Set-Cookie: [JSESSIONID=EAC894290BAE5618DEEA90ED16627DCA; Path=/; HttpOnly] + Transfer-Encoding: [chunked] + Vary: [User-Agent] + X-AREQUESTID: [527x876x1] + X-ASEN: [SEN-L13384799] + X-ASESSIONID: [1wxvpx8] + X-AUSERNAME: [gbaconnier] + X-Content-Type-Options: [nosniff] + X-Frame-Options: [SAMEORIGIN] + X-Seraph-LoginReason: [OK] + X-XSS-Protection: [1; mode=block] + content-length: ['256'] + status: {code: 200, message: ''} +- request: + body: null + headers: + Accept: + - !!binary | + YXBwbGljYXRpb24vanNvbiwqLio7cT0wLjk= + Accept-Encoding: + - !!binary | + Z3ppcCwgZGVmbGF0ZQ== + Cache-Control: + - !!binary | + bm8tY2FjaGU= + Connection: + - !!binary | + a2VlcC1hbGl2ZQ== + Content-Type: + - !!binary | + YXBwbGljYXRpb24vanNvbg== + User-Agent: + - !!binary | + cHl0aG9uLXJlcXVlc3RzLzIuMjEuMA== + X-Atlassian-Token: + - !!binary | + bm8tY2hlY2s= + method: GET + uri: http://jira:8080/rest/api/2/serverInfo + response: + body: {string: '{"baseUrl":"http://localhost:8080","version":"7.12.3","versionNumbers":[7,12,3],"deploymentType":"Server","buildNumber":712004,"buildDate":"2018-10-12T00:00:00.000+0000","serverTime":"2019-04-04T08:47:51.278+0000","scmInfo":"5ef91d760d7124da5ebec5c16a948a4a807698df","serverTitle":"Jira"}'} + headers: + Cache-Control: ['no-cache, no-store, no-transform'] + Content-Security-Policy: [frame-ancestors 'self'] + Content-Type: [application/json;charset=UTF-8] + Date: ['Thu, 04 Apr 2019 08:47:50 GMT'] + Set-Cookie: [JSESSIONID=ACF11B7F5582201B91581B11F31637FA; Path=/; HttpOnly, + atlassian.xsrf.token=BYG3-6SPF-0UM1-2LBO_0d796386486fa2c80ade6e265d8b6d1c5339bb65_lin; + Path=/] + Transfer-Encoding: [chunked] + Vary: [User-Agent] + X-AREQUESTID: [527x877x1] + X-ASEN: [SEN-L13384799] + X-ASESSIONID: [c2163r] + X-AUSERNAME: [gbaconnier] + X-Content-Type-Options: [nosniff] + X-Frame-Options: [SAMEORIGIN] + X-Seraph-LoginReason: [OK] + X-XSS-Protection: [1; mode=block] + content-length: ['288'] + status: {code: 200, message: ''} +- request: + body: null + headers: + Accept: + - !!binary | + YXBwbGljYXRpb24vanNvbiwqLio7cT0wLjk= + Accept-Encoding: + - !!binary | + Z3ppcCwgZGVmbGF0ZQ== + Cache-Control: + - !!binary | + bm8tY2FjaGU= + Connection: + - !!binary | + a2VlcC1hbGl2ZQ== + Content-Type: + - !!binary | + YXBwbGljYXRpb24vanNvbg== + Cookie: + - !!binary | + SlNFU1NJT05JRD1BQ0YxMUI3RjU1ODIyMDFCOTE1ODFCMTFGMzE2MzdGQTsgYXRsYXNzaWFuLnhz + cmYudG9rZW49QllHMy02U1BGLTBVTTEtMkxCT18wZDc5NjM4NjQ4NmZhMmM4MGFkZTZlMjY1ZDhi + NmQxYzUzMzliYjY1X2xpbg== + User-Agent: + - !!binary | + cHl0aG9uLXJlcXVlc3RzLzIuMjEuMA== + X-Atlassian-Token: + - !!binary | + bm8tY2hlY2s= + method: GET + uri: http://jira:8080/rest/api/2/field + response: + body: {string: "[{\"id\":\"issuetype\",\"name\":\"Issue Type\",\"custom\":false,\"\ + orderable\":true,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"\ + issuetype\",\"type\"],\"schema\":{\"type\":\"issuetype\",\"system\":\"issuetype\"\ + }},{\"id\":\"components\",\"name\":\"Component/s\",\"custom\":false,\"orderable\"\ + :true,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"component\"\ + ],\"schema\":{\"type\":\"array\",\"items\":\"component\",\"system\":\"components\"\ + }},{\"id\":\"issuekey\",\"name\":\"Key\",\"custom\":false,\"orderable\":false,\"\ + navigable\":true,\"searchable\":false,\"clauseNames\":[\"id\",\"issue\",\"\ + issuekey\",\"key\"]},{\"id\":\"timespent\",\"name\":\"Time Spent\",\"custom\"\ + :false,\"orderable\":false,\"navigable\":true,\"searchable\":false,\"clauseNames\"\ + :[\"timespent\"],\"schema\":{\"type\":\"number\",\"system\":\"timespent\"\ + }},{\"id\":\"timeoriginalestimate\",\"name\":\"Original Estimate\",\"custom\"\ + :false,\"orderable\":false,\"navigable\":true,\"searchable\":false,\"clauseNames\"\ + :[\"originalEstimate\",\"timeoriginalestimate\"],\"schema\":{\"type\":\"number\"\ + ,\"system\":\"timeoriginalestimate\"}},{\"id\":\"project\",\"name\":\"Project\"\ + ,\"custom\":false,\"orderable\":false,\"navigable\":true,\"searchable\":true,\"\ + clauseNames\":[\"project\"],\"schema\":{\"type\":\"project\",\"system\":\"\ + project\"}},{\"id\":\"description\",\"name\":\"Description\",\"custom\":false,\"\ + orderable\":true,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"\ + description\"],\"schema\":{\"type\":\"string\",\"system\":\"description\"\ + }},{\"id\":\"fixVersions\",\"name\":\"Fix Version/s\",\"custom\":false,\"\ + orderable\":true,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"\ + fixVersion\"],\"schema\":{\"type\":\"array\",\"items\":\"version\",\"system\"\ + :\"fixVersions\"}},{\"id\":\"aggregatetimespent\",\"name\":\"\u03A3 Time Spent\"\ + ,\"custom\":false,\"orderable\":false,\"navigable\":true,\"searchable\":false,\"\ + clauseNames\":[],\"schema\":{\"type\":\"number\",\"system\":\"aggregatetimespent\"\ + }},{\"id\":\"resolution\",\"name\":\"Resolution\",\"custom\":false,\"orderable\"\ + :true,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"resolution\"\ + ],\"schema\":{\"type\":\"resolution\",\"system\":\"resolution\"}},{\"id\"\ + :\"timetracking\",\"name\":\"Time Tracking\",\"custom\":false,\"orderable\"\ + :true,\"navigable\":false,\"searchable\":true,\"clauseNames\":[],\"schema\"\ + :{\"type\":\"timetracking\",\"system\":\"timetracking\"}},{\"id\":\"customfield_10104\"\ + ,\"name\":\"Epic Color\",\"custom\":true,\"orderable\":true,\"navigable\"\ + :true,\"searchable\":true,\"clauseNames\":[\"cf[10104]\",\"Epic Color\"],\"\ + schema\":{\"type\":\"string\",\"custom\":\"com.pyxis.greenhopper.jira:gh-epic-color\"\ + ,\"customId\":10104}},{\"id\":\"customfield_10105\",\"name\":\"Rank\",\"custom\"\ + :true,\"orderable\":true,\"navigable\":true,\"searchable\":true,\"clauseNames\"\ + :[\"cf[10105]\",\"Rank\"],\"schema\":{\"type\":\"any\",\"custom\":\"com.pyxis.greenhopper.jira:gh-lexo-rank\"\ + ,\"customId\":10105}},{\"id\":\"security\",\"name\":\"Security Level\",\"\ + custom\":false,\"orderable\":true,\"navigable\":true,\"searchable\":true,\"\ + clauseNames\":[\"level\"],\"schema\":{\"type\":\"securitylevel\",\"system\"\ + :\"security\"}},{\"id\":\"customfield_10106\",\"name\":\"Story Points\",\"\ + custom\":true,\"orderable\":true,\"navigable\":true,\"searchable\":true,\"\ + clauseNames\":[\"cf[10106]\",\"Story Points\"],\"schema\":{\"type\":\"number\"\ + ,\"custom\":\"com.atlassian.jira.plugin.system.customfieldtypes:float\",\"\ + customId\":10106}},{\"id\":\"attachment\",\"name\":\"Attachment\",\"custom\"\ + :false,\"orderable\":true,\"navigable\":false,\"searchable\":true,\"clauseNames\"\ + :[\"attachments\"],\"schema\":{\"type\":\"array\",\"items\":\"attachment\"\ + ,\"system\":\"attachment\"}},{\"id\":\"aggregatetimeestimate\",\"name\":\"\ + \u03A3 Remaining Estimate\",\"custom\":false,\"orderable\":false,\"navigable\"\ + :true,\"searchable\":false,\"clauseNames\":[],\"schema\":{\"type\":\"number\"\ + ,\"system\":\"aggregatetimeestimate\"}},{\"id\":\"resolutiondate\",\"name\"\ + :\"Resolved\",\"custom\":false,\"orderable\":false,\"navigable\":true,\"searchable\"\ + :true,\"clauseNames\":[\"resolutiondate\",\"resolved\"],\"schema\":{\"type\"\ + :\"datetime\",\"system\":\"resolutiondate\"}},{\"id\":\"workratio\",\"name\"\ + :\"Work Ratio\",\"custom\":false,\"orderable\":false,\"navigable\":true,\"\ + searchable\":true,\"clauseNames\":[\"workratio\"],\"schema\":{\"type\":\"\ + number\",\"system\":\"workratio\"}},{\"id\":\"summary\",\"name\":\"Summary\"\ + ,\"custom\":false,\"orderable\":true,\"navigable\":true,\"searchable\":true,\"\ + clauseNames\":[\"summary\"],\"schema\":{\"type\":\"string\",\"system\":\"\ + summary\"}},{\"id\":\"lastViewed\",\"name\":\"Last Viewed\",\"custom\":false,\"\ + orderable\":false,\"navigable\":true,\"searchable\":false,\"clauseNames\"\ + :[\"lastViewed\"],\"schema\":{\"type\":\"datetime\",\"system\":\"lastViewed\"\ + }},{\"id\":\"watches\",\"name\":\"Watchers\",\"custom\":false,\"orderable\"\ + :false,\"navigable\":true,\"searchable\":false,\"clauseNames\":[\"watchers\"\ + ],\"schema\":{\"type\":\"watches\",\"system\":\"watches\"}},{\"id\":\"creator\"\ + ,\"name\":\"Creator\",\"custom\":false,\"orderable\":false,\"navigable\":true,\"\ + searchable\":true,\"clauseNames\":[\"creator\"],\"schema\":{\"type\":\"user\"\ + ,\"system\":\"creator\"}},{\"id\":\"thumbnail\",\"name\":\"Images\",\"custom\"\ + :false,\"orderable\":false,\"navigable\":true,\"searchable\":false,\"clauseNames\"\ + :[]},{\"id\":\"subtasks\",\"name\":\"Sub-Tasks\",\"custom\":false,\"orderable\"\ + :false,\"navigable\":true,\"searchable\":false,\"clauseNames\":[\"subtasks\"\ + ],\"schema\":{\"type\":\"array\",\"items\":\"issuelinks\",\"system\":\"subtasks\"\ + }},{\"id\":\"created\",\"name\":\"Created\",\"custom\":false,\"orderable\"\ + :false,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"created\"\ + ,\"createdDate\"],\"schema\":{\"type\":\"datetime\",\"system\":\"created\"\ + }},{\"id\":\"reporter\",\"name\":\"Reporter\",\"custom\":false,\"orderable\"\ + :true,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"reporter\"\ + ],\"schema\":{\"type\":\"user\",\"system\":\"reporter\"}},{\"id\":\"aggregateprogress\"\ + ,\"name\":\"\u03A3 Progress\",\"custom\":false,\"orderable\":false,\"navigable\"\ + :true,\"searchable\":false,\"clauseNames\":[],\"schema\":{\"type\":\"progress\"\ + ,\"system\":\"aggregateprogress\"}},{\"id\":\"customfield_10000\",\"name\"\ + :\"Development\",\"custom\":true,\"orderable\":true,\"navigable\":true,\"\ + searchable\":true,\"clauseNames\":[\"cf[10000]\",\"Development\"],\"schema\"\ + :{\"type\":\"any\",\"custom\":\"com.atlassian.jira.plugins.jira-development-integration-plugin:devsummary\"\ + ,\"customId\":10000}},{\"id\":\"priority\",\"name\":\"Priority\",\"custom\"\ + :false,\"orderable\":true,\"navigable\":true,\"searchable\":true,\"clauseNames\"\ + :[\"priority\"],\"schema\":{\"type\":\"priority\",\"system\":\"priority\"\ + }},{\"id\":\"customfield_10100\",\"name\":\"Sprint\",\"custom\":true,\"orderable\"\ + :true,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"cf[10100]\"\ + ,\"Sprint\"],\"schema\":{\"type\":\"array\",\"items\":\"string\",\"custom\"\ + :\"com.pyxis.greenhopper.jira:gh-sprint\",\"customId\":10100}},{\"id\":\"\ + customfield_10101\",\"name\":\"Epic Link\",\"custom\":true,\"orderable\":true,\"\ + navigable\":true,\"searchable\":true,\"clauseNames\":[\"cf[10101]\",\"Epic\ + \ Link\"],\"schema\":{\"type\":\"any\",\"custom\":\"com.pyxis.greenhopper.jira:gh-epic-link\"\ + ,\"customId\":10101}},{\"id\":\"customfield_10102\",\"name\":\"Epic Status\"\ + ,\"custom\":true,\"orderable\":true,\"navigable\":true,\"searchable\":true,\"\ + clauseNames\":[\"cf[10102]\",\"Epic Status\"],\"schema\":{\"type\":\"option\"\ + ,\"custom\":\"com.pyxis.greenhopper.jira:gh-epic-status\",\"customId\":10102}},{\"\ + id\":\"labels\",\"name\":\"Labels\",\"custom\":false,\"orderable\":true,\"\ + navigable\":true,\"searchable\":true,\"clauseNames\":[\"labels\"],\"schema\"\ + :{\"type\":\"array\",\"items\":\"string\",\"system\":\"labels\"}},{\"id\"\ + :\"customfield_10103\",\"name\":\"Epic Name\",\"custom\":true,\"orderable\"\ + :true,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"cf[10103]\"\ + ,\"Epic Name\"],\"schema\":{\"type\":\"string\",\"custom\":\"com.pyxis.greenhopper.jira:gh-epic-label\"\ + ,\"customId\":10103}},{\"id\":\"environment\",\"name\":\"Environment\",\"\ + custom\":false,\"orderable\":true,\"navigable\":true,\"searchable\":true,\"\ + clauseNames\":[\"environment\"],\"schema\":{\"type\":\"string\",\"system\"\ + :\"environment\"}},{\"id\":\"timeestimate\",\"name\":\"Remaining Estimate\"\ + ,\"custom\":false,\"orderable\":false,\"navigable\":true,\"searchable\":false,\"\ + clauseNames\":[\"remainingEstimate\",\"timeestimate\"],\"schema\":{\"type\"\ + :\"number\",\"system\":\"timeestimate\"}},{\"id\":\"aggregatetimeoriginalestimate\"\ + ,\"name\":\"\u03A3 Original Estimate\",\"custom\":false,\"orderable\":false,\"\ + navigable\":true,\"searchable\":false,\"clauseNames\":[],\"schema\":{\"type\"\ + :\"number\",\"system\":\"aggregatetimeoriginalestimate\"}},{\"id\":\"versions\"\ + ,\"name\":\"Affects Version/s\",\"custom\":false,\"orderable\":true,\"navigable\"\ + :true,\"searchable\":true,\"clauseNames\":[\"affectedVersion\"],\"schema\"\ + :{\"type\":\"array\",\"items\":\"version\",\"system\":\"versions\"}},{\"id\"\ + :\"duedate\",\"name\":\"Due Date\",\"custom\":false,\"orderable\":true,\"\ + navigable\":true,\"searchable\":true,\"clauseNames\":[\"due\",\"duedate\"\ + ],\"schema\":{\"type\":\"date\",\"system\":\"duedate\"}},{\"id\":\"progress\"\ + ,\"name\":\"Progress\",\"custom\":false,\"orderable\":false,\"navigable\"\ + :true,\"searchable\":false,\"clauseNames\":[\"progress\"],\"schema\":{\"type\"\ + :\"progress\",\"system\":\"progress\"}},{\"id\":\"comment\",\"name\":\"Comment\"\ + ,\"custom\":false,\"orderable\":true,\"navigable\":false,\"searchable\":true,\"\ + clauseNames\":[\"comment\"],\"schema\":{\"type\":\"comments-page\",\"system\"\ + :\"comment\"}},{\"id\":\"votes\",\"name\":\"Votes\",\"custom\":false,\"orderable\"\ + :false,\"navigable\":true,\"searchable\":false,\"clauseNames\":[\"votes\"\ + ],\"schema\":{\"type\":\"votes\",\"system\":\"votes\"}},{\"id\":\"issuelinks\"\ + ,\"name\":\"Linked Issues\",\"custom\":false,\"orderable\":true,\"navigable\"\ + :true,\"searchable\":true,\"clauseNames\":[],\"schema\":{\"type\":\"array\"\ + ,\"items\":\"issuelinks\",\"system\":\"issuelinks\"}},{\"id\":\"worklog\"\ + ,\"name\":\"Log Work\",\"custom\":false,\"orderable\":true,\"navigable\":false,\"\ + searchable\":true,\"clauseNames\":[],\"schema\":{\"type\":\"array\",\"items\"\ + :\"worklog\",\"system\":\"worklog\"}},{\"id\":\"assignee\",\"name\":\"Assignee\"\ + ,\"custom\":false,\"orderable\":true,\"navigable\":true,\"searchable\":true,\"\ + clauseNames\":[\"assignee\"],\"schema\":{\"type\":\"user\",\"system\":\"assignee\"\ + }},{\"id\":\"updated\",\"name\":\"Updated\",\"custom\":false,\"orderable\"\ + :false,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"updated\"\ + ,\"updatedDate\"],\"schema\":{\"type\":\"datetime\",\"system\":\"updated\"\ + }},{\"id\":\"status\",\"name\":\"Status\",\"custom\":false,\"orderable\":false,\"\ + navigable\":true,\"searchable\":true,\"clauseNames\":[\"status\"],\"schema\"\ + :{\"type\":\"status\",\"system\":\"status\"}}]"} + headers: + Cache-Control: ['no-cache, no-store, no-transform'] + Content-Security-Policy: [frame-ancestors 'self'] + Content-Type: [application/json;charset=UTF-8] + Date: ['Thu, 04 Apr 2019 08:47:50 GMT'] + Set-Cookie: [JSESSIONID=91B78D3112BCBA30F361CA3D894981F0; Path=/; HttpOnly] + Transfer-Encoding: [chunked] + Vary: [User-Agent] + X-AREQUESTID: [527x878x1] + X-ASEN: [SEN-L13384799] + X-ASESSIONID: [18xj5fc] + X-AUSERNAME: [gbaconnier] + X-Content-Type-Options: [nosniff] + X-Frame-Options: [SAMEORIGIN] + X-Seraph-LoginReason: [OK] + X-XSS-Protection: [1; mode=block] + content-length: ['9557'] + status: {code: 200, message: ''} +- request: + body: null + headers: + Accept: + - !!binary | + YXBwbGljYXRpb24vanNvbiwqLio7cT0wLjk= + Accept-Encoding: + - !!binary | + Z3ppcCwgZGVmbGF0ZQ== + Cache-Control: + - !!binary | + bm8tY2FjaGU= + Connection: + - !!binary | + a2VlcC1hbGl2ZQ== + Content-Type: + - !!binary | + YXBwbGljYXRpb24vanNvbg== + Cookie: + - !!binary | + SlNFU1NJT05JRD05MUI3OEQzMTEyQkNCQTMwRjM2MUNBM0Q4OTQ5ODFGMDsgYXRsYXNzaWFuLnhz + cmYudG9rZW49QllHMy02U1BGLTBVTTEtMkxCT18wZDc5NjM4NjQ4NmZhMmM4MGFkZTZlMjY1ZDhi + NmQxYzUzMzliYjY1X2xpbg== + User-Agent: + - !!binary | + cHl0aG9uLXJlcXVlc3RzLzIuMjEuMA== + X-Atlassian-Token: + - !!binary | + bm8tY2hlY2s= + method: GET + uri: http://jira:8080/rest/api/2/issuetype/10004 + response: + body: {string: '{"self":"http://jira:8080/rest/api/2/issuetype/10004","id":"10004","description":"A + problem which impairs or prevents the functions of the product.","iconUrl":"http://jira:8080/secure/viewavatar?size=xsmall&avatarId=10303&avatarType=issuetype","name":"Bug","subtask":false,"avatarId":10303}'} + headers: + Cache-Control: ['no-cache, no-store, no-transform'] + Content-Security-Policy: [frame-ancestors 'self'] + Content-Type: [application/json;charset=UTF-8] + Date: ['Thu, 04 Apr 2019 08:47:50 GMT'] + Set-Cookie: [JSESSIONID=9717EDAECB93F99C8AA9B5300684D7AE; Path=/; HttpOnly] + Transfer-Encoding: [chunked] + Vary: [User-Agent] + X-AREQUESTID: [527x879x1] + X-ASEN: [SEN-L13384799] + X-ASESSIONID: [1n3azds] + X-AUSERNAME: [gbaconnier] + X-Content-Type-Options: [nosniff] + X-Frame-Options: [SAMEORIGIN] + X-Seraph-LoginReason: [OK] + X-XSS-Protection: [1; mode=block] + content-length: ['290'] + status: {code: 200, message: ''} +- request: + body: null + headers: + Accept: + - !!binary | + YXBwbGljYXRpb24vanNvbiwqLio7cT0wLjk= + Accept-Encoding: + - !!binary | + Z3ppcCwgZGVmbGF0ZQ== + Cache-Control: + - !!binary | + bm8tY2FjaGU= + Connection: + - !!binary | + a2VlcC1hbGl2ZQ== + Content-Type: + - !!binary | + YXBwbGljYXRpb24vanNvbg== + User-Agent: + - !!binary | + cHl0aG9uLXJlcXVlc3RzLzIuMjEuMA== + X-Atlassian-Token: + - !!binary | + bm8tY2hlY2s= + method: GET + uri: http://jira:8080/rest/api/2/serverInfo + response: + body: {string: '{"baseUrl":"http://localhost:8080","version":"7.12.3","versionNumbers":[7,12,3],"deploymentType":"Server","buildNumber":712004,"buildDate":"2018-10-12T00:00:00.000+0000","serverTime":"2019-04-04T08:47:51.355+0000","scmInfo":"5ef91d760d7124da5ebec5c16a948a4a807698df","serverTitle":"Jira"}'} + headers: + Cache-Control: ['no-cache, no-store, no-transform'] + Content-Security-Policy: [frame-ancestors 'self'] + Content-Type: [application/json;charset=UTF-8] + Date: ['Thu, 04 Apr 2019 08:47:50 GMT'] + Set-Cookie: [JSESSIONID=71E64EB8460FC6084682A409CCC54D0F; Path=/; HttpOnly, + atlassian.xsrf.token=BYG3-6SPF-0UM1-2LBO_58499d4673de026e97ade13f4b59cb151f60092c_lin; + Path=/] + Transfer-Encoding: [chunked] + Vary: [User-Agent] + X-AREQUESTID: [527x880x1] + X-ASEN: [SEN-L13384799] + X-ASESSIONID: [5a2fr7] + X-AUSERNAME: [gbaconnier] + X-Content-Type-Options: [nosniff] + X-Frame-Options: [SAMEORIGIN] + X-Seraph-LoginReason: [OK] + X-XSS-Protection: [1; mode=block] + content-length: ['288'] + status: {code: 200, message: ''} +- request: + body: null + headers: + Accept: + - !!binary | + YXBwbGljYXRpb24vanNvbiwqLio7cT0wLjk= + Accept-Encoding: + - !!binary | + Z3ppcCwgZGVmbGF0ZQ== + Cache-Control: + - !!binary | + bm8tY2FjaGU= + Connection: + - !!binary | + a2VlcC1hbGl2ZQ== + Content-Type: + - !!binary | + YXBwbGljYXRpb24vanNvbg== + Cookie: + - !!binary | + SlNFU1NJT05JRD03MUU2NEVCODQ2MEZDNjA4NDY4MkE0MDlDQ0M1NEQwRjsgYXRsYXNzaWFuLnhz + cmYudG9rZW49QllHMy02U1BGLTBVTTEtMkxCT181ODQ5OWQ0NjczZGUwMjZlOTdhZGUxM2Y0YjU5 + Y2IxNTFmNjAwOTJjX2xpbg== + User-Agent: + - !!binary | + cHl0aG9uLXJlcXVlc3RzLzIuMjEuMA== + X-Atlassian-Token: + - !!binary | + bm8tY2hlY2s= + method: GET + uri: http://jira:8080/rest/api/2/field + response: + body: {string: "[{\"id\":\"issuetype\",\"name\":\"Issue Type\",\"custom\":false,\"\ + orderable\":true,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"\ + issuetype\",\"type\"],\"schema\":{\"type\":\"issuetype\",\"system\":\"issuetype\"\ + }},{\"id\":\"components\",\"name\":\"Component/s\",\"custom\":false,\"orderable\"\ + :true,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"component\"\ + ],\"schema\":{\"type\":\"array\",\"items\":\"component\",\"system\":\"components\"\ + }},{\"id\":\"issuekey\",\"name\":\"Key\",\"custom\":false,\"orderable\":false,\"\ + navigable\":true,\"searchable\":false,\"clauseNames\":[\"id\",\"issue\",\"\ + issuekey\",\"key\"]},{\"id\":\"timespent\",\"name\":\"Time Spent\",\"custom\"\ + :false,\"orderable\":false,\"navigable\":true,\"searchable\":false,\"clauseNames\"\ + :[\"timespent\"],\"schema\":{\"type\":\"number\",\"system\":\"timespent\"\ + }},{\"id\":\"timeoriginalestimate\",\"name\":\"Original Estimate\",\"custom\"\ + :false,\"orderable\":false,\"navigable\":true,\"searchable\":false,\"clauseNames\"\ + :[\"originalEstimate\",\"timeoriginalestimate\"],\"schema\":{\"type\":\"number\"\ + ,\"system\":\"timeoriginalestimate\"}},{\"id\":\"project\",\"name\":\"Project\"\ + ,\"custom\":false,\"orderable\":false,\"navigable\":true,\"searchable\":true,\"\ + clauseNames\":[\"project\"],\"schema\":{\"type\":\"project\",\"system\":\"\ + project\"}},{\"id\":\"description\",\"name\":\"Description\",\"custom\":false,\"\ + orderable\":true,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"\ + description\"],\"schema\":{\"type\":\"string\",\"system\":\"description\"\ + }},{\"id\":\"fixVersions\",\"name\":\"Fix Version/s\",\"custom\":false,\"\ + orderable\":true,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"\ + fixVersion\"],\"schema\":{\"type\":\"array\",\"items\":\"version\",\"system\"\ + :\"fixVersions\"}},{\"id\":\"aggregatetimespent\",\"name\":\"\u03A3 Time Spent\"\ + ,\"custom\":false,\"orderable\":false,\"navigable\":true,\"searchable\":false,\"\ + clauseNames\":[],\"schema\":{\"type\":\"number\",\"system\":\"aggregatetimespent\"\ + }},{\"id\":\"resolution\",\"name\":\"Resolution\",\"custom\":false,\"orderable\"\ + :true,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"resolution\"\ + ],\"schema\":{\"type\":\"resolution\",\"system\":\"resolution\"}},{\"id\"\ + :\"timetracking\",\"name\":\"Time Tracking\",\"custom\":false,\"orderable\"\ + :true,\"navigable\":false,\"searchable\":true,\"clauseNames\":[],\"schema\"\ + :{\"type\":\"timetracking\",\"system\":\"timetracking\"}},{\"id\":\"customfield_10104\"\ + ,\"name\":\"Epic Color\",\"custom\":true,\"orderable\":true,\"navigable\"\ + :true,\"searchable\":true,\"clauseNames\":[\"cf[10104]\",\"Epic Color\"],\"\ + schema\":{\"type\":\"string\",\"custom\":\"com.pyxis.greenhopper.jira:gh-epic-color\"\ + ,\"customId\":10104}},{\"id\":\"customfield_10105\",\"name\":\"Rank\",\"custom\"\ + :true,\"orderable\":true,\"navigable\":true,\"searchable\":true,\"clauseNames\"\ + :[\"cf[10105]\",\"Rank\"],\"schema\":{\"type\":\"any\",\"custom\":\"com.pyxis.greenhopper.jira:gh-lexo-rank\"\ + ,\"customId\":10105}},{\"id\":\"security\",\"name\":\"Security Level\",\"\ + custom\":false,\"orderable\":true,\"navigable\":true,\"searchable\":true,\"\ + clauseNames\":[\"level\"],\"schema\":{\"type\":\"securitylevel\",\"system\"\ + :\"security\"}},{\"id\":\"customfield_10106\",\"name\":\"Story Points\",\"\ + custom\":true,\"orderable\":true,\"navigable\":true,\"searchable\":true,\"\ + clauseNames\":[\"cf[10106]\",\"Story Points\"],\"schema\":{\"type\":\"number\"\ + ,\"custom\":\"com.atlassian.jira.plugin.system.customfieldtypes:float\",\"\ + customId\":10106}},{\"id\":\"attachment\",\"name\":\"Attachment\",\"custom\"\ + :false,\"orderable\":true,\"navigable\":false,\"searchable\":true,\"clauseNames\"\ + :[\"attachments\"],\"schema\":{\"type\":\"array\",\"items\":\"attachment\"\ + ,\"system\":\"attachment\"}},{\"id\":\"aggregatetimeestimate\",\"name\":\"\ + \u03A3 Remaining Estimate\",\"custom\":false,\"orderable\":false,\"navigable\"\ + :true,\"searchable\":false,\"clauseNames\":[],\"schema\":{\"type\":\"number\"\ + ,\"system\":\"aggregatetimeestimate\"}},{\"id\":\"resolutiondate\",\"name\"\ + :\"Resolved\",\"custom\":false,\"orderable\":false,\"navigable\":true,\"searchable\"\ + :true,\"clauseNames\":[\"resolutiondate\",\"resolved\"],\"schema\":{\"type\"\ + :\"datetime\",\"system\":\"resolutiondate\"}},{\"id\":\"workratio\",\"name\"\ + :\"Work Ratio\",\"custom\":false,\"orderable\":false,\"navigable\":true,\"\ + searchable\":true,\"clauseNames\":[\"workratio\"],\"schema\":{\"type\":\"\ + number\",\"system\":\"workratio\"}},{\"id\":\"summary\",\"name\":\"Summary\"\ + ,\"custom\":false,\"orderable\":true,\"navigable\":true,\"searchable\":true,\"\ + clauseNames\":[\"summary\"],\"schema\":{\"type\":\"string\",\"system\":\"\ + summary\"}},{\"id\":\"lastViewed\",\"name\":\"Last Viewed\",\"custom\":false,\"\ + orderable\":false,\"navigable\":true,\"searchable\":false,\"clauseNames\"\ + :[\"lastViewed\"],\"schema\":{\"type\":\"datetime\",\"system\":\"lastViewed\"\ + }},{\"id\":\"watches\",\"name\":\"Watchers\",\"custom\":false,\"orderable\"\ + :false,\"navigable\":true,\"searchable\":false,\"clauseNames\":[\"watchers\"\ + ],\"schema\":{\"type\":\"watches\",\"system\":\"watches\"}},{\"id\":\"creator\"\ + ,\"name\":\"Creator\",\"custom\":false,\"orderable\":false,\"navigable\":true,\"\ + searchable\":true,\"clauseNames\":[\"creator\"],\"schema\":{\"type\":\"user\"\ + ,\"system\":\"creator\"}},{\"id\":\"thumbnail\",\"name\":\"Images\",\"custom\"\ + :false,\"orderable\":false,\"navigable\":true,\"searchable\":false,\"clauseNames\"\ + :[]},{\"id\":\"subtasks\",\"name\":\"Sub-Tasks\",\"custom\":false,\"orderable\"\ + :false,\"navigable\":true,\"searchable\":false,\"clauseNames\":[\"subtasks\"\ + ],\"schema\":{\"type\":\"array\",\"items\":\"issuelinks\",\"system\":\"subtasks\"\ + }},{\"id\":\"created\",\"name\":\"Created\",\"custom\":false,\"orderable\"\ + :false,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"created\"\ + ,\"createdDate\"],\"schema\":{\"type\":\"datetime\",\"system\":\"created\"\ + }},{\"id\":\"reporter\",\"name\":\"Reporter\",\"custom\":false,\"orderable\"\ + :true,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"reporter\"\ + ],\"schema\":{\"type\":\"user\",\"system\":\"reporter\"}},{\"id\":\"aggregateprogress\"\ + ,\"name\":\"\u03A3 Progress\",\"custom\":false,\"orderable\":false,\"navigable\"\ + :true,\"searchable\":false,\"clauseNames\":[],\"schema\":{\"type\":\"progress\"\ + ,\"system\":\"aggregateprogress\"}},{\"id\":\"customfield_10000\",\"name\"\ + :\"Development\",\"custom\":true,\"orderable\":true,\"navigable\":true,\"\ + searchable\":true,\"clauseNames\":[\"cf[10000]\",\"Development\"],\"schema\"\ + :{\"type\":\"any\",\"custom\":\"com.atlassian.jira.plugins.jira-development-integration-plugin:devsummary\"\ + ,\"customId\":10000}},{\"id\":\"priority\",\"name\":\"Priority\",\"custom\"\ + :false,\"orderable\":true,\"navigable\":true,\"searchable\":true,\"clauseNames\"\ + :[\"priority\"],\"schema\":{\"type\":\"priority\",\"system\":\"priority\"\ + }},{\"id\":\"customfield_10100\",\"name\":\"Sprint\",\"custom\":true,\"orderable\"\ + :true,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"cf[10100]\"\ + ,\"Sprint\"],\"schema\":{\"type\":\"array\",\"items\":\"string\",\"custom\"\ + :\"com.pyxis.greenhopper.jira:gh-sprint\",\"customId\":10100}},{\"id\":\"\ + customfield_10101\",\"name\":\"Epic Link\",\"custom\":true,\"orderable\":true,\"\ + navigable\":true,\"searchable\":true,\"clauseNames\":[\"cf[10101]\",\"Epic\ + \ Link\"],\"schema\":{\"type\":\"any\",\"custom\":\"com.pyxis.greenhopper.jira:gh-epic-link\"\ + ,\"customId\":10101}},{\"id\":\"customfield_10102\",\"name\":\"Epic Status\"\ + ,\"custom\":true,\"orderable\":true,\"navigable\":true,\"searchable\":true,\"\ + clauseNames\":[\"cf[10102]\",\"Epic Status\"],\"schema\":{\"type\":\"option\"\ + ,\"custom\":\"com.pyxis.greenhopper.jira:gh-epic-status\",\"customId\":10102}},{\"\ + id\":\"labels\",\"name\":\"Labels\",\"custom\":false,\"orderable\":true,\"\ + navigable\":true,\"searchable\":true,\"clauseNames\":[\"labels\"],\"schema\"\ + :{\"type\":\"array\",\"items\":\"string\",\"system\":\"labels\"}},{\"id\"\ + :\"customfield_10103\",\"name\":\"Epic Name\",\"custom\":true,\"orderable\"\ + :true,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"cf[10103]\"\ + ,\"Epic Name\"],\"schema\":{\"type\":\"string\",\"custom\":\"com.pyxis.greenhopper.jira:gh-epic-label\"\ + ,\"customId\":10103}},{\"id\":\"environment\",\"name\":\"Environment\",\"\ + custom\":false,\"orderable\":true,\"navigable\":true,\"searchable\":true,\"\ + clauseNames\":[\"environment\"],\"schema\":{\"type\":\"string\",\"system\"\ + :\"environment\"}},{\"id\":\"timeestimate\",\"name\":\"Remaining Estimate\"\ + ,\"custom\":false,\"orderable\":false,\"navigable\":true,\"searchable\":false,\"\ + clauseNames\":[\"remainingEstimate\",\"timeestimate\"],\"schema\":{\"type\"\ + :\"number\",\"system\":\"timeestimate\"}},{\"id\":\"aggregatetimeoriginalestimate\"\ + ,\"name\":\"\u03A3 Original Estimate\",\"custom\":false,\"orderable\":false,\"\ + navigable\":true,\"searchable\":false,\"clauseNames\":[],\"schema\":{\"type\"\ + :\"number\",\"system\":\"aggregatetimeoriginalestimate\"}},{\"id\":\"versions\"\ + ,\"name\":\"Affects Version/s\",\"custom\":false,\"orderable\":true,\"navigable\"\ + :true,\"searchable\":true,\"clauseNames\":[\"affectedVersion\"],\"schema\"\ + :{\"type\":\"array\",\"items\":\"version\",\"system\":\"versions\"}},{\"id\"\ + :\"duedate\",\"name\":\"Due Date\",\"custom\":false,\"orderable\":true,\"\ + navigable\":true,\"searchable\":true,\"clauseNames\":[\"due\",\"duedate\"\ + ],\"schema\":{\"type\":\"date\",\"system\":\"duedate\"}},{\"id\":\"progress\"\ + ,\"name\":\"Progress\",\"custom\":false,\"orderable\":false,\"navigable\"\ + :true,\"searchable\":false,\"clauseNames\":[\"progress\"],\"schema\":{\"type\"\ + :\"progress\",\"system\":\"progress\"}},{\"id\":\"comment\",\"name\":\"Comment\"\ + ,\"custom\":false,\"orderable\":true,\"navigable\":false,\"searchable\":true,\"\ + clauseNames\":[\"comment\"],\"schema\":{\"type\":\"comments-page\",\"system\"\ + :\"comment\"}},{\"id\":\"votes\",\"name\":\"Votes\",\"custom\":false,\"orderable\"\ + :false,\"navigable\":true,\"searchable\":false,\"clauseNames\":[\"votes\"\ + ],\"schema\":{\"type\":\"votes\",\"system\":\"votes\"}},{\"id\":\"issuelinks\"\ + ,\"name\":\"Linked Issues\",\"custom\":false,\"orderable\":true,\"navigable\"\ + :true,\"searchable\":true,\"clauseNames\":[],\"schema\":{\"type\":\"array\"\ + ,\"items\":\"issuelinks\",\"system\":\"issuelinks\"}},{\"id\":\"worklog\"\ + ,\"name\":\"Log Work\",\"custom\":false,\"orderable\":true,\"navigable\":false,\"\ + searchable\":true,\"clauseNames\":[],\"schema\":{\"type\":\"array\",\"items\"\ + :\"worklog\",\"system\":\"worklog\"}},{\"id\":\"assignee\",\"name\":\"Assignee\"\ + ,\"custom\":false,\"orderable\":true,\"navigable\":true,\"searchable\":true,\"\ + clauseNames\":[\"assignee\"],\"schema\":{\"type\":\"user\",\"system\":\"assignee\"\ + }},{\"id\":\"updated\",\"name\":\"Updated\",\"custom\":false,\"orderable\"\ + :false,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"updated\"\ + ,\"updatedDate\"],\"schema\":{\"type\":\"datetime\",\"system\":\"updated\"\ + }},{\"id\":\"status\",\"name\":\"Status\",\"custom\":false,\"orderable\":false,\"\ + navigable\":true,\"searchable\":true,\"clauseNames\":[\"status\"],\"schema\"\ + :{\"type\":\"status\",\"system\":\"status\"}}]"} + headers: + Cache-Control: ['no-cache, no-store, no-transform'] + Content-Security-Policy: [frame-ancestors 'self'] + Content-Type: [application/json;charset=UTF-8] + Date: ['Thu, 04 Apr 2019 08:47:50 GMT'] + Set-Cookie: [JSESSIONID=DFEB7770E9D27B2E88E1693E54937AF2; Path=/; HttpOnly] + Transfer-Encoding: [chunked] + Vary: [User-Agent] + X-AREQUESTID: [527x881x1] + X-ASEN: [SEN-L13384799] + X-ASESSIONID: [gsma0i] + X-AUSERNAME: [gbaconnier] + X-Content-Type-Options: [nosniff] + X-Frame-Options: [SAMEORIGIN] + X-Seraph-LoginReason: [OK] + X-XSS-Protection: [1; mode=block] + content-length: ['9557'] + status: {code: 200, message: ''} +- request: + body: null + headers: + Accept: + - !!binary | + YXBwbGljYXRpb24vanNvbiwqLio7cT0wLjk= + Accept-Encoding: + - !!binary | + Z3ppcCwgZGVmbGF0ZQ== + Cache-Control: + - !!binary | + bm8tY2FjaGU= + Connection: + - !!binary | + a2VlcC1hbGl2ZQ== + Content-Type: + - !!binary | + YXBwbGljYXRpb24vanNvbg== + Cookie: + - !!binary | + SlNFU1NJT05JRD1ERkVCNzc3MEU5RDI3QjJFODhFMTY5M0U1NDkzN0FGMjsgYXRsYXNzaWFuLnhz + cmYudG9rZW49QllHMy02U1BGLTBVTTEtMkxCT181ODQ5OWQ0NjczZGUwMjZlOTdhZGUxM2Y0YjU5 + Y2IxNTFmNjAwOTJjX2xpbg== + User-Agent: + - !!binary | + cHl0aG9uLXJlcXVlc3RzLzIuMjEuMA== + X-Atlassian-Token: + - !!binary | + bm8tY2hlY2s= + method: GET + uri: http://jira:8080/rest/api/2/issuetype/10000 + response: + body: {string: '{"self":"http://jira:8080/rest/api/2/issuetype/10000","id":"10000","description":"Created + by Jira Software - do not edit or delete. Issue type for a big user story + that needs to be broken down.","iconUrl":"http://jira:8080/images/icons/issuetypes/epic.svg","name":"Epic","subtask":false}'} + headers: + Cache-Control: ['no-cache, no-store, no-transform'] + Content-Security-Policy: [frame-ancestors 'self'] + Content-Type: [application/json;charset=UTF-8] + Date: ['Thu, 04 Apr 2019 08:47:50 GMT'] + Set-Cookie: [JSESSIONID=CFC29C747288A6659FA67FDC373E6DF5; Path=/; HttpOnly] + Transfer-Encoding: [chunked] + Vary: [User-Agent] + X-AREQUESTID: [527x882x1] + X-ASEN: [SEN-L13384799] + X-ASESSIONID: [45y3el] + X-AUSERNAME: [gbaconnier] + X-Content-Type-Options: [nosniff] + X-Frame-Options: [SAMEORIGIN] + X-Seraph-LoginReason: [OK] + X-XSS-Protection: [1; mode=block] + content-length: ['287'] + status: {code: 200, message: ''} +version: 1 diff --git a/connector_jira/tests/fixtures/cassettes/test_import_task_epic.yaml b/connector_jira/tests/fixtures/cassettes/test_import_task_epic.yaml new file mode 100644 index 000000000..07557ee09 --- /dev/null +++ b/connector_jira/tests/fixtures/cassettes/test_import_task_epic.yaml @@ -0,0 +1,308 @@ +interactions: +- request: + body: null + headers: + Accept: + - !!binary | + YXBwbGljYXRpb24vanNvbiwqLio7cT0wLjk= + Accept-Encoding: + - !!binary | + Z3ppcCwgZGVmbGF0ZQ== + Cache-Control: + - !!binary | + bm8tY2FjaGU= + Connection: + - !!binary | + a2VlcC1hbGl2ZQ== + Content-Type: + - !!binary | + YXBwbGljYXRpb24vanNvbg== + User-Agent: + - !!binary | + cHl0aG9uLXJlcXVlc3RzLzIuMjEuMA== + X-Atlassian-Token: + - !!binary | + bm8tY2hlY2s= + method: GET + uri: http://jira:8080/rest/api/2/serverInfo + response: + body: {string: '{"baseUrl":"http://localhost:8080","version":"7.12.3","versionNumbers":[7,12,3],"deploymentType":"Server","buildNumber":712004,"buildDate":"2018-10-12T00:00:00.000+0000","serverTime":"2019-04-04T09:33:04.341+0000","scmInfo":"5ef91d760d7124da5ebec5c16a948a4a807698df","serverTitle":"Jira"}'} + headers: + Cache-Control: ['no-cache, no-store, no-transform'] + Content-Security-Policy: [frame-ancestors 'self'] + Content-Type: [application/json;charset=UTF-8] + Date: ['Thu, 04 Apr 2019 09:33:04 GMT'] + Set-Cookie: [JSESSIONID=16C31E5808CB7DC2BE90F32982704017; Path=/; HttpOnly, + atlassian.xsrf.token=BYG3-6SPF-0UM1-2LBO_b6b900d177346e0363fd626b2e36ef03638e58e3_lin; + Path=/] + Transfer-Encoding: [chunked] + Vary: [User-Agent] + X-AREQUESTID: [573x1049x1] + X-ASEN: [SEN-L13384799] + X-ASESSIONID: [1vjpr4s] + X-AUSERNAME: [gbaconnier] + X-Content-Type-Options: [nosniff] + X-Frame-Options: [SAMEORIGIN] + X-Seraph-LoginReason: [OK] + X-XSS-Protection: [1; mode=block] + content-length: ['288'] + status: {code: 200, message: ''} +- request: + body: null + headers: + Accept: + - !!binary | + YXBwbGljYXRpb24vanNvbiwqLio7cT0wLjk= + Accept-Encoding: + - !!binary | + Z3ppcCwgZGVmbGF0ZQ== + Cache-Control: + - !!binary | + bm8tY2FjaGU= + Connection: + - !!binary | + a2VlcC1hbGl2ZQ== + Content-Type: + - !!binary | + YXBwbGljYXRpb24vanNvbg== + Cookie: + - !!binary | + SlNFU1NJT05JRD0xNkMzMUU1ODA4Q0I3REMyQkU5MEYzMjk4MjcwNDAxNzsgYXRsYXNzaWFuLnhz + cmYudG9rZW49QllHMy02U1BGLTBVTTEtMkxCT19iNmI5MDBkMTc3MzQ2ZTAzNjNmZDYyNmIyZTM2 + ZWYwMzYzOGU1OGUzX2xpbg== + User-Agent: + - !!binary | + cHl0aG9uLXJlcXVlc3RzLzIuMjEuMA== + X-Atlassian-Token: + - !!binary | + bm8tY2hlY2s= + method: GET + uri: http://jira:8080/rest/api/2/field + response: + body: {string: "[{\"id\":\"issuetype\",\"name\":\"Issue Type\",\"custom\":false,\"\ + orderable\":true,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"\ + issuetype\",\"type\"],\"schema\":{\"type\":\"issuetype\",\"system\":\"issuetype\"\ + }},{\"id\":\"components\",\"name\":\"Component/s\",\"custom\":false,\"orderable\"\ + :true,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"component\"\ + ],\"schema\":{\"type\":\"array\",\"items\":\"component\",\"system\":\"components\"\ + }},{\"id\":\"issuekey\",\"name\":\"Key\",\"custom\":false,\"orderable\":false,\"\ + navigable\":true,\"searchable\":false,\"clauseNames\":[\"id\",\"issue\",\"\ + issuekey\",\"key\"]},{\"id\":\"timespent\",\"name\":\"Time Spent\",\"custom\"\ + :false,\"orderable\":false,\"navigable\":true,\"searchable\":false,\"clauseNames\"\ + :[\"timespent\"],\"schema\":{\"type\":\"number\",\"system\":\"timespent\"\ + }},{\"id\":\"timeoriginalestimate\",\"name\":\"Original Estimate\",\"custom\"\ + :false,\"orderable\":false,\"navigable\":true,\"searchable\":false,\"clauseNames\"\ + :[\"originalEstimate\",\"timeoriginalestimate\"],\"schema\":{\"type\":\"number\"\ + ,\"system\":\"timeoriginalestimate\"}},{\"id\":\"project\",\"name\":\"Project\"\ + ,\"custom\":false,\"orderable\":false,\"navigable\":true,\"searchable\":true,\"\ + clauseNames\":[\"project\"],\"schema\":{\"type\":\"project\",\"system\":\"\ + project\"}},{\"id\":\"description\",\"name\":\"Description\",\"custom\":false,\"\ + orderable\":true,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"\ + description\"],\"schema\":{\"type\":\"string\",\"system\":\"description\"\ + }},{\"id\":\"fixVersions\",\"name\":\"Fix Version/s\",\"custom\":false,\"\ + orderable\":true,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"\ + fixVersion\"],\"schema\":{\"type\":\"array\",\"items\":\"version\",\"system\"\ + :\"fixVersions\"}},{\"id\":\"aggregatetimespent\",\"name\":\"\u03A3 Time Spent\"\ + ,\"custom\":false,\"orderable\":false,\"navigable\":true,\"searchable\":false,\"\ + clauseNames\":[],\"schema\":{\"type\":\"number\",\"system\":\"aggregatetimespent\"\ + }},{\"id\":\"resolution\",\"name\":\"Resolution\",\"custom\":false,\"orderable\"\ + :true,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"resolution\"\ + ],\"schema\":{\"type\":\"resolution\",\"system\":\"resolution\"}},{\"id\"\ + :\"timetracking\",\"name\":\"Time Tracking\",\"custom\":false,\"orderable\"\ + :true,\"navigable\":false,\"searchable\":true,\"clauseNames\":[],\"schema\"\ + :{\"type\":\"timetracking\",\"system\":\"timetracking\"}},{\"id\":\"customfield_10104\"\ + ,\"name\":\"Epic Color\",\"custom\":true,\"orderable\":true,\"navigable\"\ + :true,\"searchable\":true,\"clauseNames\":[\"cf[10104]\",\"Epic Color\"],\"\ + schema\":{\"type\":\"string\",\"custom\":\"com.pyxis.greenhopper.jira:gh-epic-color\"\ + ,\"customId\":10104}},{\"id\":\"customfield_10105\",\"name\":\"Rank\",\"custom\"\ + :true,\"orderable\":true,\"navigable\":true,\"searchable\":true,\"clauseNames\"\ + :[\"cf[10105]\",\"Rank\"],\"schema\":{\"type\":\"any\",\"custom\":\"com.pyxis.greenhopper.jira:gh-lexo-rank\"\ + ,\"customId\":10105}},{\"id\":\"security\",\"name\":\"Security Level\",\"\ + custom\":false,\"orderable\":true,\"navigable\":true,\"searchable\":true,\"\ + clauseNames\":[\"level\"],\"schema\":{\"type\":\"securitylevel\",\"system\"\ + :\"security\"}},{\"id\":\"customfield_10106\",\"name\":\"Story Points\",\"\ + custom\":true,\"orderable\":true,\"navigable\":true,\"searchable\":true,\"\ + clauseNames\":[\"cf[10106]\",\"Story Points\"],\"schema\":{\"type\":\"number\"\ + ,\"custom\":\"com.atlassian.jira.plugin.system.customfieldtypes:float\",\"\ + customId\":10106}},{\"id\":\"attachment\",\"name\":\"Attachment\",\"custom\"\ + :false,\"orderable\":true,\"navigable\":false,\"searchable\":true,\"clauseNames\"\ + :[\"attachments\"],\"schema\":{\"type\":\"array\",\"items\":\"attachment\"\ + ,\"system\":\"attachment\"}},{\"id\":\"aggregatetimeestimate\",\"name\":\"\ + \u03A3 Remaining Estimate\",\"custom\":false,\"orderable\":false,\"navigable\"\ + :true,\"searchable\":false,\"clauseNames\":[],\"schema\":{\"type\":\"number\"\ + ,\"system\":\"aggregatetimeestimate\"}},{\"id\":\"resolutiondate\",\"name\"\ + :\"Resolved\",\"custom\":false,\"orderable\":false,\"navigable\":true,\"searchable\"\ + :true,\"clauseNames\":[\"resolutiondate\",\"resolved\"],\"schema\":{\"type\"\ + :\"datetime\",\"system\":\"resolutiondate\"}},{\"id\":\"workratio\",\"name\"\ + :\"Work Ratio\",\"custom\":false,\"orderable\":false,\"navigable\":true,\"\ + searchable\":true,\"clauseNames\":[\"workratio\"],\"schema\":{\"type\":\"\ + number\",\"system\":\"workratio\"}},{\"id\":\"summary\",\"name\":\"Summary\"\ + ,\"custom\":false,\"orderable\":true,\"navigable\":true,\"searchable\":true,\"\ + clauseNames\":[\"summary\"],\"schema\":{\"type\":\"string\",\"system\":\"\ + summary\"}},{\"id\":\"lastViewed\",\"name\":\"Last Viewed\",\"custom\":false,\"\ + orderable\":false,\"navigable\":true,\"searchable\":false,\"clauseNames\"\ + :[\"lastViewed\"],\"schema\":{\"type\":\"datetime\",\"system\":\"lastViewed\"\ + }},{\"id\":\"watches\",\"name\":\"Watchers\",\"custom\":false,\"orderable\"\ + :false,\"navigable\":true,\"searchable\":false,\"clauseNames\":[\"watchers\"\ + ],\"schema\":{\"type\":\"watches\",\"system\":\"watches\"}},{\"id\":\"creator\"\ + ,\"name\":\"Creator\",\"custom\":false,\"orderable\":false,\"navigable\":true,\"\ + searchable\":true,\"clauseNames\":[\"creator\"],\"schema\":{\"type\":\"user\"\ + ,\"system\":\"creator\"}},{\"id\":\"thumbnail\",\"name\":\"Images\",\"custom\"\ + :false,\"orderable\":false,\"navigable\":true,\"searchable\":false,\"clauseNames\"\ + :[]},{\"id\":\"subtasks\",\"name\":\"Sub-Tasks\",\"custom\":false,\"orderable\"\ + :false,\"navigable\":true,\"searchable\":false,\"clauseNames\":[\"subtasks\"\ + ],\"schema\":{\"type\":\"array\",\"items\":\"issuelinks\",\"system\":\"subtasks\"\ + }},{\"id\":\"created\",\"name\":\"Created\",\"custom\":false,\"orderable\"\ + :false,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"created\"\ + ,\"createdDate\"],\"schema\":{\"type\":\"datetime\",\"system\":\"created\"\ + }},{\"id\":\"reporter\",\"name\":\"Reporter\",\"custom\":false,\"orderable\"\ + :true,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"reporter\"\ + ],\"schema\":{\"type\":\"user\",\"system\":\"reporter\"}},{\"id\":\"aggregateprogress\"\ + ,\"name\":\"\u03A3 Progress\",\"custom\":false,\"orderable\":false,\"navigable\"\ + :true,\"searchable\":false,\"clauseNames\":[],\"schema\":{\"type\":\"progress\"\ + ,\"system\":\"aggregateprogress\"}},{\"id\":\"customfield_10000\",\"name\"\ + :\"Development\",\"custom\":true,\"orderable\":true,\"navigable\":true,\"\ + searchable\":true,\"clauseNames\":[\"cf[10000]\",\"Development\"],\"schema\"\ + :{\"type\":\"any\",\"custom\":\"com.atlassian.jira.plugins.jira-development-integration-plugin:devsummary\"\ + ,\"customId\":10000}},{\"id\":\"priority\",\"name\":\"Priority\",\"custom\"\ + :false,\"orderable\":true,\"navigable\":true,\"searchable\":true,\"clauseNames\"\ + :[\"priority\"],\"schema\":{\"type\":\"priority\",\"system\":\"priority\"\ + }},{\"id\":\"customfield_10100\",\"name\":\"Sprint\",\"custom\":true,\"orderable\"\ + :true,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"cf[10100]\"\ + ,\"Sprint\"],\"schema\":{\"type\":\"array\",\"items\":\"string\",\"custom\"\ + :\"com.pyxis.greenhopper.jira:gh-sprint\",\"customId\":10100}},{\"id\":\"\ + customfield_10101\",\"name\":\"Epic Link\",\"custom\":true,\"orderable\":true,\"\ + navigable\":true,\"searchable\":true,\"clauseNames\":[\"cf[10101]\",\"Epic\ + \ Link\"],\"schema\":{\"type\":\"any\",\"custom\":\"com.pyxis.greenhopper.jira:gh-epic-link\"\ + ,\"customId\":10101}},{\"id\":\"customfield_10102\",\"name\":\"Epic Status\"\ + ,\"custom\":true,\"orderable\":true,\"navigable\":true,\"searchable\":true,\"\ + clauseNames\":[\"cf[10102]\",\"Epic Status\"],\"schema\":{\"type\":\"option\"\ + ,\"custom\":\"com.pyxis.greenhopper.jira:gh-epic-status\",\"customId\":10102}},{\"\ + id\":\"labels\",\"name\":\"Labels\",\"custom\":false,\"orderable\":true,\"\ + navigable\":true,\"searchable\":true,\"clauseNames\":[\"labels\"],\"schema\"\ + :{\"type\":\"array\",\"items\":\"string\",\"system\":\"labels\"}},{\"id\"\ + :\"customfield_10103\",\"name\":\"Epic Name\",\"custom\":true,\"orderable\"\ + :true,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"cf[10103]\"\ + ,\"Epic Name\"],\"schema\":{\"type\":\"string\",\"custom\":\"com.pyxis.greenhopper.jira:gh-epic-label\"\ + ,\"customId\":10103}},{\"id\":\"environment\",\"name\":\"Environment\",\"\ + custom\":false,\"orderable\":true,\"navigable\":true,\"searchable\":true,\"\ + clauseNames\":[\"environment\"],\"schema\":{\"type\":\"string\",\"system\"\ + :\"environment\"}},{\"id\":\"timeestimate\",\"name\":\"Remaining Estimate\"\ + ,\"custom\":false,\"orderable\":false,\"navigable\":true,\"searchable\":false,\"\ + clauseNames\":[\"remainingEstimate\",\"timeestimate\"],\"schema\":{\"type\"\ + :\"number\",\"system\":\"timeestimate\"}},{\"id\":\"aggregatetimeoriginalestimate\"\ + ,\"name\":\"\u03A3 Original Estimate\",\"custom\":false,\"orderable\":false,\"\ + navigable\":true,\"searchable\":false,\"clauseNames\":[],\"schema\":{\"type\"\ + :\"number\",\"system\":\"aggregatetimeoriginalestimate\"}},{\"id\":\"versions\"\ + ,\"name\":\"Affects Version/s\",\"custom\":false,\"orderable\":true,\"navigable\"\ + :true,\"searchable\":true,\"clauseNames\":[\"affectedVersion\"],\"schema\"\ + :{\"type\":\"array\",\"items\":\"version\",\"system\":\"versions\"}},{\"id\"\ + :\"duedate\",\"name\":\"Due Date\",\"custom\":false,\"orderable\":true,\"\ + navigable\":true,\"searchable\":true,\"clauseNames\":[\"due\",\"duedate\"\ + ],\"schema\":{\"type\":\"date\",\"system\":\"duedate\"}},{\"id\":\"progress\"\ + ,\"name\":\"Progress\",\"custom\":false,\"orderable\":false,\"navigable\"\ + :true,\"searchable\":false,\"clauseNames\":[\"progress\"],\"schema\":{\"type\"\ + :\"progress\",\"system\":\"progress\"}},{\"id\":\"comment\",\"name\":\"Comment\"\ + ,\"custom\":false,\"orderable\":true,\"navigable\":false,\"searchable\":true,\"\ + clauseNames\":[\"comment\"],\"schema\":{\"type\":\"comments-page\",\"system\"\ + :\"comment\"}},{\"id\":\"votes\",\"name\":\"Votes\",\"custom\":false,\"orderable\"\ + :false,\"navigable\":true,\"searchable\":false,\"clauseNames\":[\"votes\"\ + ],\"schema\":{\"type\":\"votes\",\"system\":\"votes\"}},{\"id\":\"issuelinks\"\ + ,\"name\":\"Linked Issues\",\"custom\":false,\"orderable\":true,\"navigable\"\ + :true,\"searchable\":true,\"clauseNames\":[],\"schema\":{\"type\":\"array\"\ + ,\"items\":\"issuelinks\",\"system\":\"issuelinks\"}},{\"id\":\"worklog\"\ + ,\"name\":\"Log Work\",\"custom\":false,\"orderable\":true,\"navigable\":false,\"\ + searchable\":true,\"clauseNames\":[],\"schema\":{\"type\":\"array\",\"items\"\ + :\"worklog\",\"system\":\"worklog\"}},{\"id\":\"assignee\",\"name\":\"Assignee\"\ + ,\"custom\":false,\"orderable\":true,\"navigable\":true,\"searchable\":true,\"\ + clauseNames\":[\"assignee\"],\"schema\":{\"type\":\"user\",\"system\":\"assignee\"\ + }},{\"id\":\"updated\",\"name\":\"Updated\",\"custom\":false,\"orderable\"\ + :false,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"updated\"\ + ,\"updatedDate\"],\"schema\":{\"type\":\"datetime\",\"system\":\"updated\"\ + }},{\"id\":\"status\",\"name\":\"Status\",\"custom\":false,\"orderable\":false,\"\ + navigable\":true,\"searchable\":true,\"clauseNames\":[\"status\"],\"schema\"\ + :{\"type\":\"status\",\"system\":\"status\"}}]"} + headers: + Cache-Control: ['no-cache, no-store, no-transform'] + Content-Security-Policy: [frame-ancestors 'self'] + Content-Type: [application/json;charset=UTF-8] + Date: ['Thu, 04 Apr 2019 09:33:04 GMT'] + Set-Cookie: [JSESSIONID=103E6970668C42F7C55C544F732AF930; Path=/; HttpOnly] + Transfer-Encoding: [chunked] + Vary: [User-Agent] + X-AREQUESTID: [573x1050x1] + X-ASEN: [SEN-L13384799] + X-ASESSIONID: [zopd3y] + X-AUSERNAME: [gbaconnier] + X-Content-Type-Options: [nosniff] + X-Frame-Options: [SAMEORIGIN] + X-Seraph-LoginReason: [OK] + X-XSS-Protection: [1; mode=block] + content-length: ['9557'] + status: {code: 200, message: ''} +- request: + body: null + headers: + Accept: + - !!binary | + YXBwbGljYXRpb24vanNvbiwqLio7cT0wLjk= + Accept-Encoding: + - !!binary | + Z3ppcCwgZGVmbGF0ZQ== + Cache-Control: + - !!binary | + bm8tY2FjaGU= + Connection: + - !!binary | + a2VlcC1hbGl2ZQ== + Content-Type: + - !!binary | + YXBwbGljYXRpb24vanNvbg== + Cookie: + - !!binary | + SlNFU1NJT05JRD0xMDNFNjk3MDY2OEM0MkY3QzU1QzU0NEY3MzJBRjkzMDsgYXRsYXNzaWFuLnhz + cmYudG9rZW49QllHMy02U1BGLTBVTTEtMkxCT19iNmI5MDBkMTc3MzQ2ZTAzNjNmZDYyNmIyZTM2 + ZWYwMzYzOGU1OGUzX2xpbg== + User-Agent: + - !!binary | + cHl0aG9uLXJlcXVlc3RzLzIuMjEuMA== + X-Atlassian-Token: + - !!binary | + bm8tY2hlY2s= + method: GET + uri: http://jira:8080/rest/api/2/issue/10000 + response: + body: {string: '{"expand":"renderedFields,names,schema,operations,editmeta,changelog,versionedRepresentations","id":"10000","self":"http://jira:8080/rest/api/2/issue/10000","key":"TEST-1","fields":{"issuetype":{"self":"http://jira:8080/rest/api/2/issuetype/10000","id":"10000","description":"Created + by Jira Software - do not edit or delete. Issue type for a big user story + that needs to be broken down.","iconUrl":"http://jira:8080/images/icons/issuetypes/epic.svg","name":"Epic","subtask":false},"components":[],"timespent":null,"timeoriginalestimate":null,"description":null,"project":{"self":"http://jira:8080/rest/api/2/project/10000","id":"10000","key":"TEST","name":"TEST","avatarUrls":{"48x48":"http://jira:8080/secure/projectavatar?avatarId=10324","24x24":"http://jira:8080/secure/projectavatar?size=small&avatarId=10324","16x16":"http://jira:8080/secure/projectavatar?size=xsmall&avatarId=10324","32x32":"http://jira:8080/secure/projectavatar?size=medium&avatarId=10324"}},"fixVersions":[],"aggregatetimespent":null,"resolution":null,"timetracking":{},"customfield_10104":"ghx-label-1","customfield_10105":"0|hzzzzz:","customfield_10106":null,"attachment":[],"aggregatetimeestimate":null,"resolutiondate":null,"workratio":-1,"summary":"Epic1","lastViewed":"2019-04-04T09:31:34.589+0000","watches":{"self":"http://jira:8080/rest/api/2/issue/TEST-1/watchers","watchCount":1,"isWatching":true},"creator":{"self":"http://jira:8080/rest/api/2/user?username=gbaconnier","name":"gbaconnier","key":"gbaconnier","emailAddress":"guewen.baconnier@camptocamp.com","avatarUrls":{"48x48":"https://www.gravatar.com/avatar/ad252192c3f73885676b7d2e850ad13c?d=mm&s=48","24x24":"https://www.gravatar.com/avatar/ad252192c3f73885676b7d2e850ad13c?d=mm&s=24","16x16":"https://www.gravatar.com/avatar/ad252192c3f73885676b7d2e850ad13c?d=mm&s=16","32x32":"https://www.gravatar.com/avatar/ad252192c3f73885676b7d2e850ad13c?d=mm&s=32"},"displayName":"Guewen + Baconnier","active":true,"timeZone":"GMT"},"subtasks":[],"created":"2019-04-04T09:31:27.779+0000","reporter":{"self":"http://jira:8080/rest/api/2/user?username=gbaconnier","name":"gbaconnier","key":"gbaconnier","emailAddress":"guewen.baconnier@camptocamp.com","avatarUrls":{"48x48":"https://www.gravatar.com/avatar/ad252192c3f73885676b7d2e850ad13c?d=mm&s=48","24x24":"https://www.gravatar.com/avatar/ad252192c3f73885676b7d2e850ad13c?d=mm&s=24","16x16":"https://www.gravatar.com/avatar/ad252192c3f73885676b7d2e850ad13c?d=mm&s=16","32x32":"https://www.gravatar.com/avatar/ad252192c3f73885676b7d2e850ad13c?d=mm&s=32"},"displayName":"Guewen + Baconnier","active":true,"timeZone":"GMT"},"customfield_10000":"{summaryBean=com.atlassian.jira.plugin.devstatus.rest.SummaryBean@181ea90d[summary={pullrequest=com.atlassian.jira.plugin.devstatus.rest.SummaryItemBean@56542810[overall=PullRequestOverallBean{stateCount=0, + state=''OPEN'', details=PullRequestOverallDetails{openCount=0, mergedCount=0, + declinedCount=0}},byInstanceType={}], build=com.atlassian.jira.plugin.devstatus.rest.SummaryItemBean@5f98e72[overall=com.atlassian.jira.plugin.devstatus.summary.beans.BuildOverallBean@729a2275[failedBuildCount=0,successfulBuildCount=0,unknownBuildCount=0,count=0,lastUpdated=,lastUpdatedTimestamp=],byInstanceType={}], + review=com.atlassian.jira.plugin.devstatus.rest.SummaryItemBean@1a21969c[overall=com.atlassian.jira.plugin.devstatus.summary.beans.ReviewsOverallBean@62bec6ca[stateCount=0,state=,dueDate=,overDue=false,count=0,lastUpdated=,lastUpdatedTimestamp=],byInstanceType={}], + deployment-environment=com.atlassian.jira.plugin.devstatus.rest.SummaryItemBean@2b7d4ef1[overall=com.atlassian.jira.plugin.devstatus.summary.beans.DeploymentOverallBean@8d75861[topEnvironments=[],showProjects=false,successfulCount=0,count=0,lastUpdated=,lastUpdatedTimestamp=],byInstanceType={}], + repository=com.atlassian.jira.plugin.devstatus.rest.SummaryItemBean@1ed4aa91[overall=com.atlassian.jira.plugin.devstatus.summary.beans.CommitOverallBean@a48b717[count=0,lastUpdated=,lastUpdatedTimestamp=],byInstanceType={}], + branch=com.atlassian.jira.plugin.devstatus.rest.SummaryItemBean@4b037a69[overall=com.atlassian.jira.plugin.devstatus.summary.beans.BranchOverallBean@79253738[count=0,lastUpdated=,lastUpdatedTimestamp=],byInstanceType={}]},errors=[],configErrors=[]], + devSummaryJson={\"cachedValue\":{\"errors\":[],\"configErrors\":[],\"summary\":{\"pullrequest\":{\"overall\":{\"count\":0,\"lastUpdated\":null,\"stateCount\":0,\"state\":\"OPEN\",\"details\":{\"openCount\":0,\"mergedCount\":0,\"declinedCount\":0,\"total\":0},\"open\":true},\"byInstanceType\":{}},\"build\":{\"overall\":{\"count\":0,\"lastUpdated\":null,\"failedBuildCount\":0,\"successfulBuildCount\":0,\"unknownBuildCount\":0},\"byInstanceType\":{}},\"review\":{\"overall\":{\"count\":0,\"lastUpdated\":null,\"stateCount\":0,\"state\":null,\"dueDate\":null,\"overDue\":false,\"completed\":false},\"byInstanceType\":{}},\"deployment-environment\":{\"overall\":{\"count\":0,\"lastUpdated\":null,\"topEnvironments\":[],\"showProjects\":false,\"successfulCount\":0},\"byInstanceType\":{}},\"repository\":{\"overall\":{\"count\":0,\"lastUpdated\":null},\"byInstanceType\":{}},\"branch\":{\"overall\":{\"count\":0,\"lastUpdated\":null},\"byInstanceType\":{}}}},\"isStale\":false}}","aggregateprogress":{"progress":0,"total":0},"priority":{"self":"http://jira:8080/rest/api/2/priority/3","iconUrl":"http://jira:8080/images/icons/priorities/medium.svg","name":"Medium","id":"3"},"customfield_10100":null,"customfield_10101":null,"customfield_10102":{"self":"http://jira:8080/rest/api/2/customFieldOption/10000","value":"To + Do","id":"10000"},"labels":[],"customfield_10103":"Epic1","environment":null,"timeestimate":null,"aggregatetimeoriginalestimate":null,"versions":[],"duedate":null,"progress":{"progress":0,"total":0},"comment":{"comments":[],"maxResults":0,"total":0,"startAt":0},"issuelinks":[],"votes":{"self":"http://jira:8080/rest/api/2/issue/TEST-1/votes","votes":0,"hasVoted":false},"worklog":{"startAt":0,"maxResults":20,"total":0,"worklogs":[]},"assignee":null,"updated":"2019-04-04T09:31:27.779+0000","status":{"self":"http://jira:8080/rest/api/2/status/10000","description":"","iconUrl":"http://jira:8080/","name":"To + Do","id":"10000","statusCategory":{"self":"http://jira:8080/rest/api/2/statuscategory/2","id":2,"key":"new","colorName":"blue-gray","name":"To + Do"}}}}'} + headers: + Cache-Control: ['no-cache, no-store, no-transform'] + Content-Security-Policy: [frame-ancestors 'self'] + Content-Type: [application/json;charset=UTF-8] + Date: ['Thu, 04 Apr 2019 09:33:04 GMT'] + Set-Cookie: [JSESSIONID=DD972EE99E4EEED069AEFC1021E6609C; Path=/; HttpOnly] + Transfer-Encoding: [chunked] + Vary: [User-Agent] + X-AREQUESTID: [573x1051x1] + X-ASEN: [SEN-L13384799] + X-ASESSIONID: [1w9f8vp] + X-AUSERNAME: [gbaconnier] + X-Content-Type-Options: [nosniff] + X-Frame-Options: [SAMEORIGIN] + X-Seraph-LoginReason: [OK] + X-XSS-Protection: [1; mode=block] + content-length: ['6413'] + status: {code: 200, message: ''} +version: 1 diff --git a/connector_jira/tests/fixtures/cassettes/test_import_task_parents.yaml b/connector_jira/tests/fixtures/cassettes/test_import_task_parents.yaml new file mode 100644 index 000000000..a6bc101f9 --- /dev/null +++ b/connector_jira/tests/fixtures/cassettes/test_import_task_parents.yaml @@ -0,0 +1,923 @@ +interactions: +- request: + body: null + headers: + Accept: + - !!binary | + YXBwbGljYXRpb24vanNvbiwqLio7cT0wLjk= + Accept-Encoding: + - !!binary | + Z3ppcCwgZGVmbGF0ZQ== + Cache-Control: + - !!binary | + bm8tY2FjaGU= + Connection: + - !!binary | + a2VlcC1hbGl2ZQ== + Content-Type: + - !!binary | + YXBwbGljYXRpb24vanNvbg== + User-Agent: + - !!binary | + cHl0aG9uLXJlcXVlc3RzLzIuMjEuMA== + X-Atlassian-Token: + - !!binary | + bm8tY2hlY2s= + method: GET + uri: http://jira:8080/rest/api/2/serverInfo + response: + body: {string: '{"baseUrl":"http://localhost:8080","version":"7.12.3","versionNumbers":[7,12,3],"deploymentType":"Server","buildNumber":712004,"buildDate":"2018-10-12T00:00:00.000+0000","serverTime":"2019-04-04T10:10:48.807+0000","scmInfo":"5ef91d760d7124da5ebec5c16a948a4a807698df","serverTitle":"Jira"}'} + headers: + Cache-Control: ['no-cache, no-store, no-transform'] + Content-Security-Policy: [frame-ancestors 'self'] + Content-Type: [application/json;charset=UTF-8] + Date: ['Thu, 04 Apr 2019 10:10:48 GMT'] + Set-Cookie: [JSESSIONID=AB1461DFF73DE6539B189396A041BF3F; Path=/; HttpOnly, + atlassian.xsrf.token=BYG3-6SPF-0UM1-2LBO_4aebc5491192d836e8a977023a6e02b6008027c3_lin; + Path=/] + Transfer-Encoding: [chunked] + Vary: [User-Agent] + X-AREQUESTID: [610x1133x1] + X-ASEN: [SEN-L13384799] + X-ASESSIONID: [xmknsz] + X-AUSERNAME: [gbaconnier] + X-Content-Type-Options: [nosniff] + X-Frame-Options: [SAMEORIGIN] + X-Seraph-LoginReason: [OK] + X-XSS-Protection: [1; mode=block] + content-length: ['288'] + status: {code: 200, message: ''} +- request: + body: null + headers: + Accept: + - !!binary | + YXBwbGljYXRpb24vanNvbiwqLio7cT0wLjk= + Accept-Encoding: + - !!binary | + Z3ppcCwgZGVmbGF0ZQ== + Cache-Control: + - !!binary | + bm8tY2FjaGU= + Connection: + - !!binary | + a2VlcC1hbGl2ZQ== + Content-Type: + - !!binary | + YXBwbGljYXRpb24vanNvbg== + Cookie: + - !!binary | + SlNFU1NJT05JRD1BQjE0NjFERkY3M0RFNjUzOUIxODkzOTZBMDQxQkYzRjsgYXRsYXNzaWFuLnhz + cmYudG9rZW49QllHMy02U1BGLTBVTTEtMkxCT180YWViYzU0OTExOTJkODM2ZThhOTc3MDIzYTZl + MDJiNjAwODAyN2MzX2xpbg== + User-Agent: + - !!binary | + cHl0aG9uLXJlcXVlc3RzLzIuMjEuMA== + X-Atlassian-Token: + - !!binary | + bm8tY2hlY2s= + method: GET + uri: http://jira:8080/rest/api/2/field + response: + body: {string: "[{\"id\":\"issuetype\",\"name\":\"Issue Type\",\"custom\":false,\"\ + orderable\":true,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"\ + issuetype\",\"type\"],\"schema\":{\"type\":\"issuetype\",\"system\":\"issuetype\"\ + }},{\"id\":\"components\",\"name\":\"Component/s\",\"custom\":false,\"orderable\"\ + :true,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"component\"\ + ],\"schema\":{\"type\":\"array\",\"items\":\"component\",\"system\":\"components\"\ + }},{\"id\":\"issuekey\",\"name\":\"Key\",\"custom\":false,\"orderable\":false,\"\ + navigable\":true,\"searchable\":false,\"clauseNames\":[\"id\",\"issue\",\"\ + issuekey\",\"key\"]},{\"id\":\"timespent\",\"name\":\"Time Spent\",\"custom\"\ + :false,\"orderable\":false,\"navigable\":true,\"searchable\":false,\"clauseNames\"\ + :[\"timespent\"],\"schema\":{\"type\":\"number\",\"system\":\"timespent\"\ + }},{\"id\":\"timeoriginalestimate\",\"name\":\"Original Estimate\",\"custom\"\ + :false,\"orderable\":false,\"navigable\":true,\"searchable\":false,\"clauseNames\"\ + :[\"originalEstimate\",\"timeoriginalestimate\"],\"schema\":{\"type\":\"number\"\ + ,\"system\":\"timeoriginalestimate\"}},{\"id\":\"project\",\"name\":\"Project\"\ + ,\"custom\":false,\"orderable\":false,\"navigable\":true,\"searchable\":true,\"\ + clauseNames\":[\"project\"],\"schema\":{\"type\":\"project\",\"system\":\"\ + project\"}},{\"id\":\"description\",\"name\":\"Description\",\"custom\":false,\"\ + orderable\":true,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"\ + description\"],\"schema\":{\"type\":\"string\",\"system\":\"description\"\ + }},{\"id\":\"fixVersions\",\"name\":\"Fix Version/s\",\"custom\":false,\"\ + orderable\":true,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"\ + fixVersion\"],\"schema\":{\"type\":\"array\",\"items\":\"version\",\"system\"\ + :\"fixVersions\"}},{\"id\":\"aggregatetimespent\",\"name\":\"\u03A3 Time Spent\"\ + ,\"custom\":false,\"orderable\":false,\"navigable\":true,\"searchable\":false,\"\ + clauseNames\":[],\"schema\":{\"type\":\"number\",\"system\":\"aggregatetimespent\"\ + }},{\"id\":\"resolution\",\"name\":\"Resolution\",\"custom\":false,\"orderable\"\ + :true,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"resolution\"\ + ],\"schema\":{\"type\":\"resolution\",\"system\":\"resolution\"}},{\"id\"\ + :\"timetracking\",\"name\":\"Time Tracking\",\"custom\":false,\"orderable\"\ + :true,\"navigable\":false,\"searchable\":true,\"clauseNames\":[],\"schema\"\ + :{\"type\":\"timetracking\",\"system\":\"timetracking\"}},{\"id\":\"customfield_10104\"\ + ,\"name\":\"Epic Color\",\"custom\":true,\"orderable\":true,\"navigable\"\ + :true,\"searchable\":true,\"clauseNames\":[\"cf[10104]\",\"Epic Color\"],\"\ + schema\":{\"type\":\"string\",\"custom\":\"com.pyxis.greenhopper.jira:gh-epic-color\"\ + ,\"customId\":10104}},{\"id\":\"customfield_10105\",\"name\":\"Rank\",\"custom\"\ + :true,\"orderable\":true,\"navigable\":true,\"searchable\":true,\"clauseNames\"\ + :[\"cf[10105]\",\"Rank\"],\"schema\":{\"type\":\"any\",\"custom\":\"com.pyxis.greenhopper.jira:gh-lexo-rank\"\ + ,\"customId\":10105}},{\"id\":\"security\",\"name\":\"Security Level\",\"\ + custom\":false,\"orderable\":true,\"navigable\":true,\"searchable\":true,\"\ + clauseNames\":[\"level\"],\"schema\":{\"type\":\"securitylevel\",\"system\"\ + :\"security\"}},{\"id\":\"customfield_10106\",\"name\":\"Story Points\",\"\ + custom\":true,\"orderable\":true,\"navigable\":true,\"searchable\":true,\"\ + clauseNames\":[\"cf[10106]\",\"Story Points\"],\"schema\":{\"type\":\"number\"\ + ,\"custom\":\"com.atlassian.jira.plugin.system.customfieldtypes:float\",\"\ + customId\":10106}},{\"id\":\"attachment\",\"name\":\"Attachment\",\"custom\"\ + :false,\"orderable\":true,\"navigable\":false,\"searchable\":true,\"clauseNames\"\ + :[\"attachments\"],\"schema\":{\"type\":\"array\",\"items\":\"attachment\"\ + ,\"system\":\"attachment\"}},{\"id\":\"aggregatetimeestimate\",\"name\":\"\ + \u03A3 Remaining Estimate\",\"custom\":false,\"orderable\":false,\"navigable\"\ + :true,\"searchable\":false,\"clauseNames\":[],\"schema\":{\"type\":\"number\"\ + ,\"system\":\"aggregatetimeestimate\"}},{\"id\":\"resolutiondate\",\"name\"\ + :\"Resolved\",\"custom\":false,\"orderable\":false,\"navigable\":true,\"searchable\"\ + :true,\"clauseNames\":[\"resolutiondate\",\"resolved\"],\"schema\":{\"type\"\ + :\"datetime\",\"system\":\"resolutiondate\"}},{\"id\":\"workratio\",\"name\"\ + :\"Work Ratio\",\"custom\":false,\"orderable\":false,\"navigable\":true,\"\ + searchable\":true,\"clauseNames\":[\"workratio\"],\"schema\":{\"type\":\"\ + number\",\"system\":\"workratio\"}},{\"id\":\"summary\",\"name\":\"Summary\"\ + ,\"custom\":false,\"orderable\":true,\"navigable\":true,\"searchable\":true,\"\ + clauseNames\":[\"summary\"],\"schema\":{\"type\":\"string\",\"system\":\"\ + summary\"}},{\"id\":\"lastViewed\",\"name\":\"Last Viewed\",\"custom\":false,\"\ + orderable\":false,\"navigable\":true,\"searchable\":false,\"clauseNames\"\ + :[\"lastViewed\"],\"schema\":{\"type\":\"datetime\",\"system\":\"lastViewed\"\ + }},{\"id\":\"watches\",\"name\":\"Watchers\",\"custom\":false,\"orderable\"\ + :false,\"navigable\":true,\"searchable\":false,\"clauseNames\":[\"watchers\"\ + ],\"schema\":{\"type\":\"watches\",\"system\":\"watches\"}},{\"id\":\"creator\"\ + ,\"name\":\"Creator\",\"custom\":false,\"orderable\":false,\"navigable\":true,\"\ + searchable\":true,\"clauseNames\":[\"creator\"],\"schema\":{\"type\":\"user\"\ + ,\"system\":\"creator\"}},{\"id\":\"thumbnail\",\"name\":\"Images\",\"custom\"\ + :false,\"orderable\":false,\"navigable\":true,\"searchable\":false,\"clauseNames\"\ + :[]},{\"id\":\"subtasks\",\"name\":\"Sub-Tasks\",\"custom\":false,\"orderable\"\ + :false,\"navigable\":true,\"searchable\":false,\"clauseNames\":[\"subtasks\"\ + ],\"schema\":{\"type\":\"array\",\"items\":\"issuelinks\",\"system\":\"subtasks\"\ + }},{\"id\":\"created\",\"name\":\"Created\",\"custom\":false,\"orderable\"\ + :false,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"created\"\ + ,\"createdDate\"],\"schema\":{\"type\":\"datetime\",\"system\":\"created\"\ + }},{\"id\":\"reporter\",\"name\":\"Reporter\",\"custom\":false,\"orderable\"\ + :true,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"reporter\"\ + ],\"schema\":{\"type\":\"user\",\"system\":\"reporter\"}},{\"id\":\"aggregateprogress\"\ + ,\"name\":\"\u03A3 Progress\",\"custom\":false,\"orderable\":false,\"navigable\"\ + :true,\"searchable\":false,\"clauseNames\":[],\"schema\":{\"type\":\"progress\"\ + ,\"system\":\"aggregateprogress\"}},{\"id\":\"customfield_10000\",\"name\"\ + :\"Development\",\"custom\":true,\"orderable\":true,\"navigable\":true,\"\ + searchable\":true,\"clauseNames\":[\"cf[10000]\",\"Development\"],\"schema\"\ + :{\"type\":\"any\",\"custom\":\"com.atlassian.jira.plugins.jira-development-integration-plugin:devsummary\"\ + ,\"customId\":10000}},{\"id\":\"priority\",\"name\":\"Priority\",\"custom\"\ + :false,\"orderable\":true,\"navigable\":true,\"searchable\":true,\"clauseNames\"\ + :[\"priority\"],\"schema\":{\"type\":\"priority\",\"system\":\"priority\"\ + }},{\"id\":\"customfield_10100\",\"name\":\"Sprint\",\"custom\":true,\"orderable\"\ + :true,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"cf[10100]\"\ + ,\"Sprint\"],\"schema\":{\"type\":\"array\",\"items\":\"string\",\"custom\"\ + :\"com.pyxis.greenhopper.jira:gh-sprint\",\"customId\":10100}},{\"id\":\"\ + customfield_10101\",\"name\":\"Epic Link\",\"custom\":true,\"orderable\":true,\"\ + navigable\":true,\"searchable\":true,\"clauseNames\":[\"cf[10101]\",\"Epic\ + \ Link\"],\"schema\":{\"type\":\"any\",\"custom\":\"com.pyxis.greenhopper.jira:gh-epic-link\"\ + ,\"customId\":10101}},{\"id\":\"customfield_10102\",\"name\":\"Epic Status\"\ + ,\"custom\":true,\"orderable\":true,\"navigable\":true,\"searchable\":true,\"\ + clauseNames\":[\"cf[10102]\",\"Epic Status\"],\"schema\":{\"type\":\"option\"\ + ,\"custom\":\"com.pyxis.greenhopper.jira:gh-epic-status\",\"customId\":10102}},{\"\ + id\":\"labels\",\"name\":\"Labels\",\"custom\":false,\"orderable\":true,\"\ + navigable\":true,\"searchable\":true,\"clauseNames\":[\"labels\"],\"schema\"\ + :{\"type\":\"array\",\"items\":\"string\",\"system\":\"labels\"}},{\"id\"\ + :\"customfield_10103\",\"name\":\"Epic Name\",\"custom\":true,\"orderable\"\ + :true,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"cf[10103]\"\ + ,\"Epic Name\"],\"schema\":{\"type\":\"string\",\"custom\":\"com.pyxis.greenhopper.jira:gh-epic-label\"\ + ,\"customId\":10103}},{\"id\":\"environment\",\"name\":\"Environment\",\"\ + custom\":false,\"orderable\":true,\"navigable\":true,\"searchable\":true,\"\ + clauseNames\":[\"environment\"],\"schema\":{\"type\":\"string\",\"system\"\ + :\"environment\"}},{\"id\":\"timeestimate\",\"name\":\"Remaining Estimate\"\ + ,\"custom\":false,\"orderable\":false,\"navigable\":true,\"searchable\":false,\"\ + clauseNames\":[\"remainingEstimate\",\"timeestimate\"],\"schema\":{\"type\"\ + :\"number\",\"system\":\"timeestimate\"}},{\"id\":\"aggregatetimeoriginalestimate\"\ + ,\"name\":\"\u03A3 Original Estimate\",\"custom\":false,\"orderable\":false,\"\ + navigable\":true,\"searchable\":false,\"clauseNames\":[],\"schema\":{\"type\"\ + :\"number\",\"system\":\"aggregatetimeoriginalestimate\"}},{\"id\":\"versions\"\ + ,\"name\":\"Affects Version/s\",\"custom\":false,\"orderable\":true,\"navigable\"\ + :true,\"searchable\":true,\"clauseNames\":[\"affectedVersion\"],\"schema\"\ + :{\"type\":\"array\",\"items\":\"version\",\"system\":\"versions\"}},{\"id\"\ + :\"duedate\",\"name\":\"Due Date\",\"custom\":false,\"orderable\":true,\"\ + navigable\":true,\"searchable\":true,\"clauseNames\":[\"due\",\"duedate\"\ + ],\"schema\":{\"type\":\"date\",\"system\":\"duedate\"}},{\"id\":\"progress\"\ + ,\"name\":\"Progress\",\"custom\":false,\"orderable\":false,\"navigable\"\ + :true,\"searchable\":false,\"clauseNames\":[\"progress\"],\"schema\":{\"type\"\ + :\"progress\",\"system\":\"progress\"}},{\"id\":\"comment\",\"name\":\"Comment\"\ + ,\"custom\":false,\"orderable\":true,\"navigable\":false,\"searchable\":true,\"\ + clauseNames\":[\"comment\"],\"schema\":{\"type\":\"comments-page\",\"system\"\ + :\"comment\"}},{\"id\":\"votes\",\"name\":\"Votes\",\"custom\":false,\"orderable\"\ + :false,\"navigable\":true,\"searchable\":false,\"clauseNames\":[\"votes\"\ + ],\"schema\":{\"type\":\"votes\",\"system\":\"votes\"}},{\"id\":\"issuelinks\"\ + ,\"name\":\"Linked Issues\",\"custom\":false,\"orderable\":true,\"navigable\"\ + :true,\"searchable\":true,\"clauseNames\":[],\"schema\":{\"type\":\"array\"\ + ,\"items\":\"issuelinks\",\"system\":\"issuelinks\"}},{\"id\":\"worklog\"\ + ,\"name\":\"Log Work\",\"custom\":false,\"orderable\":true,\"navigable\":false,\"\ + searchable\":true,\"clauseNames\":[],\"schema\":{\"type\":\"array\",\"items\"\ + :\"worklog\",\"system\":\"worklog\"}},{\"id\":\"assignee\",\"name\":\"Assignee\"\ + ,\"custom\":false,\"orderable\":true,\"navigable\":true,\"searchable\":true,\"\ + clauseNames\":[\"assignee\"],\"schema\":{\"type\":\"user\",\"system\":\"assignee\"\ + }},{\"id\":\"updated\",\"name\":\"Updated\",\"custom\":false,\"orderable\"\ + :false,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"updated\"\ + ,\"updatedDate\"],\"schema\":{\"type\":\"datetime\",\"system\":\"updated\"\ + }},{\"id\":\"status\",\"name\":\"Status\",\"custom\":false,\"orderable\":false,\"\ + navigable\":true,\"searchable\":true,\"clauseNames\":[\"status\"],\"schema\"\ + :{\"type\":\"status\",\"system\":\"status\"}}]"} + headers: + Cache-Control: ['no-cache, no-store, no-transform'] + Content-Security-Policy: [frame-ancestors 'self'] + Content-Type: [application/json;charset=UTF-8] + Date: ['Thu, 04 Apr 2019 10:10:48 GMT'] + Set-Cookie: [JSESSIONID=606C637ECD75B1D8A4E4AEABAC78B317; Path=/; HttpOnly] + Transfer-Encoding: [chunked] + Vary: [User-Agent] + X-AREQUESTID: [610x1134x1] + X-ASEN: [SEN-L13384799] + X-ASESSIONID: [hraqw7] + X-AUSERNAME: [gbaconnier] + X-Content-Type-Options: [nosniff] + X-Frame-Options: [SAMEORIGIN] + X-Seraph-LoginReason: [OK] + X-XSS-Protection: [1; mode=block] + content-length: ['9557'] + status: {code: 200, message: ''} +- request: + body: null + headers: + Accept: + - !!binary | + YXBwbGljYXRpb24vanNvbiwqLio7cT0wLjk= + Accept-Encoding: + - !!binary | + Z3ppcCwgZGVmbGF0ZQ== + Cache-Control: + - !!binary | + bm8tY2FjaGU= + Connection: + - !!binary | + a2VlcC1hbGl2ZQ== + Content-Type: + - !!binary | + YXBwbGljYXRpb24vanNvbg== + Cookie: + - !!binary | + SlNFU1NJT05JRD02MDZDNjM3RUNENzVCMUQ4QTRFNEFFQUJBQzc4QjMxNzsgYXRsYXNzaWFuLnhz + cmYudG9rZW49QllHMy02U1BGLTBVTTEtMkxCT180YWViYzU0OTExOTJkODM2ZThhOTc3MDIzYTZl + MDJiNjAwODAyN2MzX2xpbg== + User-Agent: + - !!binary | + cHl0aG9uLXJlcXVlc3RzLzIuMjEuMA== + X-Atlassian-Token: + - !!binary | + bm8tY2hlY2s= + method: GET + uri: http://jira:8080/rest/api/2/issue/10002 + response: + body: {string: '{"expand":"renderedFields,names,schema,operations,editmeta,changelog,versionedRepresentations","id":"10002","self":"http://jira:8080/rest/api/2/issue/10002","key":"TEST-3","fields":{"issuetype":{"self":"http://jira:8080/rest/api/2/issuetype/10003","id":"10003","description":"The + sub-task of the issue","iconUrl":"http://jira:8080/secure/viewavatar?size=xsmall&avatarId=10316&avatarType=issuetype","name":"Sub-task","subtask":true,"avatarId":10316},"parent":{"id":"10001","key":"TEST-2","self":"http://jira:8080/rest/api/2/issue/10001","fields":{"summary":"Task1","status":{"self":"http://jira:8080/rest/api/2/status/10000","description":"","iconUrl":"http://jira:8080/","name":"To + Do","id":"10000","statusCategory":{"self":"http://jira:8080/rest/api/2/statuscategory/2","id":2,"key":"new","colorName":"blue-gray","name":"To + Do"}},"priority":{"self":"http://jira:8080/rest/api/2/priority/3","iconUrl":"http://jira:8080/images/icons/priorities/medium.svg","name":"Medium","id":"3"},"issuetype":{"self":"http://jira:8080/rest/api/2/issuetype/10002","id":"10002","description":"A + task that needs to be done.","iconUrl":"http://jira:8080/secure/viewavatar?size=xsmall&avatarId=10318&avatarType=issuetype","name":"Task","subtask":false,"avatarId":10318}}},"components":[],"timespent":null,"timeoriginalestimate":null,"description":null,"project":{"self":"http://jira:8080/rest/api/2/project/10000","id":"10000","key":"TEST","name":"TEST","avatarUrls":{"48x48":"http://jira:8080/secure/projectavatar?avatarId=10324","24x24":"http://jira:8080/secure/projectavatar?size=small&avatarId=10324","16x16":"http://jira:8080/secure/projectavatar?size=xsmall&avatarId=10324","32x32":"http://jira:8080/secure/projectavatar?size=medium&avatarId=10324"}},"fixVersions":[],"aggregatetimespent":null,"resolution":null,"timetracking":{},"customfield_10105":"0|i0000f:","attachment":[],"aggregatetimeestimate":null,"resolutiondate":null,"workratio":-1,"summary":"Subtask1","lastViewed":"2019-04-04T09:59:12.240+0000","watches":{"self":"http://jira:8080/rest/api/2/issue/TEST-3/watchers","watchCount":1,"isWatching":true},"creator":{"self":"http://jira:8080/rest/api/2/user?username=gbaconnier","name":"gbaconnier","key":"gbaconnier","emailAddress":"guewen.baconnier@camptocamp.com","avatarUrls":{"48x48":"https://www.gravatar.com/avatar/ad252192c3f73885676b7d2e850ad13c?d=mm&s=48","24x24":"https://www.gravatar.com/avatar/ad252192c3f73885676b7d2e850ad13c?d=mm&s=24","16x16":"https://www.gravatar.com/avatar/ad252192c3f73885676b7d2e850ad13c?d=mm&s=16","32x32":"https://www.gravatar.com/avatar/ad252192c3f73885676b7d2e850ad13c?d=mm&s=32"},"displayName":"Guewen + Baconnier","active":true,"timeZone":"GMT"},"subtasks":[],"created":"2019-04-04T09:59:08.525+0000","reporter":{"self":"http://jira:8080/rest/api/2/user?username=gbaconnier","name":"gbaconnier","key":"gbaconnier","emailAddress":"guewen.baconnier@camptocamp.com","avatarUrls":{"48x48":"https://www.gravatar.com/avatar/ad252192c3f73885676b7d2e850ad13c?d=mm&s=48","24x24":"https://www.gravatar.com/avatar/ad252192c3f73885676b7d2e850ad13c?d=mm&s=24","16x16":"https://www.gravatar.com/avatar/ad252192c3f73885676b7d2e850ad13c?d=mm&s=16","32x32":"https://www.gravatar.com/avatar/ad252192c3f73885676b7d2e850ad13c?d=mm&s=32"},"displayName":"Guewen + Baconnier","active":true,"timeZone":"GMT"},"customfield_10000":"{summaryBean=com.atlassian.jira.plugin.devstatus.rest.SummaryBean@38702acb[summary={pullrequest=com.atlassian.jira.plugin.devstatus.rest.SummaryItemBean@20f9c7f8[overall=PullRequestOverallBean{stateCount=0, + state=''OPEN'', details=PullRequestOverallDetails{openCount=0, mergedCount=0, + declinedCount=0}},byInstanceType={}], build=com.atlassian.jira.plugin.devstatus.rest.SummaryItemBean@c840cb8[overall=com.atlassian.jira.plugin.devstatus.summary.beans.BuildOverallBean@62acdcd7[failedBuildCount=0,successfulBuildCount=0,unknownBuildCount=0,count=0,lastUpdated=,lastUpdatedTimestamp=],byInstanceType={}], + review=com.atlassian.jira.plugin.devstatus.rest.SummaryItemBean@67f04b5f[overall=com.atlassian.jira.plugin.devstatus.summary.beans.ReviewsOverallBean@87177cd[stateCount=0,state=,dueDate=,overDue=false,count=0,lastUpdated=,lastUpdatedTimestamp=],byInstanceType={}], + deployment-environment=com.atlassian.jira.plugin.devstatus.rest.SummaryItemBean@243c5bb7[overall=com.atlassian.jira.plugin.devstatus.summary.beans.DeploymentOverallBean@20726183[topEnvironments=[],showProjects=false,successfulCount=0,count=0,lastUpdated=,lastUpdatedTimestamp=],byInstanceType={}], + repository=com.atlassian.jira.plugin.devstatus.rest.SummaryItemBean@453d6c61[overall=com.atlassian.jira.plugin.devstatus.summary.beans.CommitOverallBean@4671b414[count=0,lastUpdated=,lastUpdatedTimestamp=],byInstanceType={}], + branch=com.atlassian.jira.plugin.devstatus.rest.SummaryItemBean@d6ae8e[overall=com.atlassian.jira.plugin.devstatus.summary.beans.BranchOverallBean@339bb96e[count=0,lastUpdated=,lastUpdatedTimestamp=],byInstanceType={}]},errors=[],configErrors=[]], + devSummaryJson={\"cachedValue\":{\"errors\":[],\"configErrors\":[],\"summary\":{\"pullrequest\":{\"overall\":{\"count\":0,\"lastUpdated\":null,\"stateCount\":0,\"state\":\"OPEN\",\"details\":{\"openCount\":0,\"mergedCount\":0,\"declinedCount\":0,\"total\":0},\"open\":true},\"byInstanceType\":{}},\"build\":{\"overall\":{\"count\":0,\"lastUpdated\":null,\"failedBuildCount\":0,\"successfulBuildCount\":0,\"unknownBuildCount\":0},\"byInstanceType\":{}},\"review\":{\"overall\":{\"count\":0,\"lastUpdated\":null,\"stateCount\":0,\"state\":null,\"dueDate\":null,\"overDue\":false,\"completed\":false},\"byInstanceType\":{}},\"deployment-environment\":{\"overall\":{\"count\":0,\"lastUpdated\":null,\"topEnvironments\":[],\"showProjects\":false,\"successfulCount\":0},\"byInstanceType\":{}},\"repository\":{\"overall\":{\"count\":0,\"lastUpdated\":null},\"byInstanceType\":{}},\"branch\":{\"overall\":{\"count\":0,\"lastUpdated\":null},\"byInstanceType\":{}}}},\"isStale\":false}}","aggregateprogress":{"progress":0,"total":0},"priority":{"self":"http://jira:8080/rest/api/2/priority/3","iconUrl":"http://jira:8080/images/icons/priorities/medium.svg","name":"Medium","id":"3"},"customfield_10100":null,"customfield_10101":null,"labels":[],"environment":null,"timeestimate":null,"aggregatetimeoriginalestimate":null,"versions":[],"duedate":null,"progress":{"progress":0,"total":0},"comment":{"comments":[],"maxResults":0,"total":0,"startAt":0},"issuelinks":[],"votes":{"self":"http://jira:8080/rest/api/2/issue/TEST-3/votes","votes":0,"hasVoted":false},"worklog":{"startAt":0,"maxResults":20,"total":0,"worklogs":[]},"assignee":null,"updated":"2019-04-04T09:59:08.525+0000","status":{"self":"http://jira:8080/rest/api/2/status/10000","description":"","iconUrl":"http://jira:8080/","name":"To + Do","id":"10000","statusCategory":{"self":"http://jira:8080/rest/api/2/statuscategory/2","id":2,"key":"new","colorName":"blue-gray","name":"To + Do"}}}}'} + headers: + Cache-Control: ['no-cache, no-store, no-transform'] + Content-Security-Policy: [frame-ancestors 'self'] + Content-Type: [application/json;charset=UTF-8] + Date: ['Thu, 04 Apr 2019 10:10:48 GMT'] + Set-Cookie: [JSESSIONID=B0A18E255C1D6A30E00ED2FBE3E1E7E1; Path=/; HttpOnly] + Transfer-Encoding: [chunked] + Vary: [User-Agent] + X-AREQUESTID: [610x1135x1] + X-ASEN: [SEN-L13384799] + X-ASESSIONID: [agcfb9] + X-AUSERNAME: [gbaconnier] + X-Content-Type-Options: [nosniff] + X-Frame-Options: [SAMEORIGIN] + X-Seraph-LoginReason: [OK] + X-XSS-Protection: [1; mode=block] + content-length: ['6985'] + status: {code: 200, message: ''} +- request: + body: null + headers: + Accept: + - !!binary | + YXBwbGljYXRpb24vanNvbiwqLio7cT0wLjk= + Accept-Encoding: + - !!binary | + Z3ppcCwgZGVmbGF0ZQ== + Cache-Control: + - !!binary | + bm8tY2FjaGU= + Connection: + - !!binary | + a2VlcC1hbGl2ZQ== + Content-Type: + - !!binary | + YXBwbGljYXRpb24vanNvbg== + User-Agent: + - !!binary | + cHl0aG9uLXJlcXVlc3RzLzIuMjEuMA== + X-Atlassian-Token: + - !!binary | + bm8tY2hlY2s= + method: GET + uri: http://jira:8080/rest/api/2/serverInfo + response: + body: {string: '{"baseUrl":"http://localhost:8080","version":"7.12.3","versionNumbers":[7,12,3],"deploymentType":"Server","buildNumber":712004,"buildDate":"2018-10-12T00:00:00.000+0000","serverTime":"2019-04-04T10:10:48.901+0000","scmInfo":"5ef91d760d7124da5ebec5c16a948a4a807698df","serverTitle":"Jira"}'} + headers: + Cache-Control: ['no-cache, no-store, no-transform'] + Content-Security-Policy: [frame-ancestors 'self'] + Content-Type: [application/json;charset=UTF-8] + Date: ['Thu, 04 Apr 2019 10:10:48 GMT'] + Set-Cookie: [JSESSIONID=16C1BB5341BB81A9F73BBDC23765201A; Path=/; HttpOnly, + atlassian.xsrf.token=BYG3-6SPF-0UM1-2LBO_f4fed1907e2708eb163901a46b3a0cb8152609b2_lin; + Path=/] + Transfer-Encoding: [chunked] + Vary: [User-Agent] + X-AREQUESTID: [610x1136x1] + X-ASEN: [SEN-L13384799] + X-ASESSIONID: [1ll3tq] + X-AUSERNAME: [gbaconnier] + X-Content-Type-Options: [nosniff] + X-Frame-Options: [SAMEORIGIN] + X-Seraph-LoginReason: [OK] + X-XSS-Protection: [1; mode=block] + content-length: ['288'] + status: {code: 200, message: ''} +- request: + body: null + headers: + Accept: + - !!binary | + YXBwbGljYXRpb24vanNvbiwqLio7cT0wLjk= + Accept-Encoding: + - !!binary | + Z3ppcCwgZGVmbGF0ZQ== + Cache-Control: + - !!binary | + bm8tY2FjaGU= + Connection: + - !!binary | + a2VlcC1hbGl2ZQ== + Content-Type: + - !!binary | + YXBwbGljYXRpb24vanNvbg== + Cookie: + - !!binary | + SlNFU1NJT05JRD0xNkMxQkI1MzQxQkI4MUE5RjczQkJEQzIzNzY1MjAxQTsgYXRsYXNzaWFuLnhz + cmYudG9rZW49QllHMy02U1BGLTBVTTEtMkxCT19mNGZlZDE5MDdlMjcwOGViMTYzOTAxYTQ2YjNh + MGNiODE1MjYwOWIyX2xpbg== + User-Agent: + - !!binary | + cHl0aG9uLXJlcXVlc3RzLzIuMjEuMA== + X-Atlassian-Token: + - !!binary | + bm8tY2hlY2s= + method: GET + uri: http://jira:8080/rest/api/2/field + response: + body: {string: "[{\"id\":\"issuetype\",\"name\":\"Issue Type\",\"custom\":false,\"\ + orderable\":true,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"\ + issuetype\",\"type\"],\"schema\":{\"type\":\"issuetype\",\"system\":\"issuetype\"\ + }},{\"id\":\"components\",\"name\":\"Component/s\",\"custom\":false,\"orderable\"\ + :true,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"component\"\ + ],\"schema\":{\"type\":\"array\",\"items\":\"component\",\"system\":\"components\"\ + }},{\"id\":\"issuekey\",\"name\":\"Key\",\"custom\":false,\"orderable\":false,\"\ + navigable\":true,\"searchable\":false,\"clauseNames\":[\"id\",\"issue\",\"\ + issuekey\",\"key\"]},{\"id\":\"timespent\",\"name\":\"Time Spent\",\"custom\"\ + :false,\"orderable\":false,\"navigable\":true,\"searchable\":false,\"clauseNames\"\ + :[\"timespent\"],\"schema\":{\"type\":\"number\",\"system\":\"timespent\"\ + }},{\"id\":\"timeoriginalestimate\",\"name\":\"Original Estimate\",\"custom\"\ + :false,\"orderable\":false,\"navigable\":true,\"searchable\":false,\"clauseNames\"\ + :[\"originalEstimate\",\"timeoriginalestimate\"],\"schema\":{\"type\":\"number\"\ + ,\"system\":\"timeoriginalestimate\"}},{\"id\":\"project\",\"name\":\"Project\"\ + ,\"custom\":false,\"orderable\":false,\"navigable\":true,\"searchable\":true,\"\ + clauseNames\":[\"project\"],\"schema\":{\"type\":\"project\",\"system\":\"\ + project\"}},{\"id\":\"description\",\"name\":\"Description\",\"custom\":false,\"\ + orderable\":true,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"\ + description\"],\"schema\":{\"type\":\"string\",\"system\":\"description\"\ + }},{\"id\":\"fixVersions\",\"name\":\"Fix Version/s\",\"custom\":false,\"\ + orderable\":true,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"\ + fixVersion\"],\"schema\":{\"type\":\"array\",\"items\":\"version\",\"system\"\ + :\"fixVersions\"}},{\"id\":\"aggregatetimespent\",\"name\":\"\u03A3 Time Spent\"\ + ,\"custom\":false,\"orderable\":false,\"navigable\":true,\"searchable\":false,\"\ + clauseNames\":[],\"schema\":{\"type\":\"number\",\"system\":\"aggregatetimespent\"\ + }},{\"id\":\"resolution\",\"name\":\"Resolution\",\"custom\":false,\"orderable\"\ + :true,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"resolution\"\ + ],\"schema\":{\"type\":\"resolution\",\"system\":\"resolution\"}},{\"id\"\ + :\"timetracking\",\"name\":\"Time Tracking\",\"custom\":false,\"orderable\"\ + :true,\"navigable\":false,\"searchable\":true,\"clauseNames\":[],\"schema\"\ + :{\"type\":\"timetracking\",\"system\":\"timetracking\"}},{\"id\":\"customfield_10104\"\ + ,\"name\":\"Epic Color\",\"custom\":true,\"orderable\":true,\"navigable\"\ + :true,\"searchable\":true,\"clauseNames\":[\"cf[10104]\",\"Epic Color\"],\"\ + schema\":{\"type\":\"string\",\"custom\":\"com.pyxis.greenhopper.jira:gh-epic-color\"\ + ,\"customId\":10104}},{\"id\":\"customfield_10105\",\"name\":\"Rank\",\"custom\"\ + :true,\"orderable\":true,\"navigable\":true,\"searchable\":true,\"clauseNames\"\ + :[\"cf[10105]\",\"Rank\"],\"schema\":{\"type\":\"any\",\"custom\":\"com.pyxis.greenhopper.jira:gh-lexo-rank\"\ + ,\"customId\":10105}},{\"id\":\"security\",\"name\":\"Security Level\",\"\ + custom\":false,\"orderable\":true,\"navigable\":true,\"searchable\":true,\"\ + clauseNames\":[\"level\"],\"schema\":{\"type\":\"securitylevel\",\"system\"\ + :\"security\"}},{\"id\":\"customfield_10106\",\"name\":\"Story Points\",\"\ + custom\":true,\"orderable\":true,\"navigable\":true,\"searchable\":true,\"\ + clauseNames\":[\"cf[10106]\",\"Story Points\"],\"schema\":{\"type\":\"number\"\ + ,\"custom\":\"com.atlassian.jira.plugin.system.customfieldtypes:float\",\"\ + customId\":10106}},{\"id\":\"attachment\",\"name\":\"Attachment\",\"custom\"\ + :false,\"orderable\":true,\"navigable\":false,\"searchable\":true,\"clauseNames\"\ + :[\"attachments\"],\"schema\":{\"type\":\"array\",\"items\":\"attachment\"\ + ,\"system\":\"attachment\"}},{\"id\":\"aggregatetimeestimate\",\"name\":\"\ + \u03A3 Remaining Estimate\",\"custom\":false,\"orderable\":false,\"navigable\"\ + :true,\"searchable\":false,\"clauseNames\":[],\"schema\":{\"type\":\"number\"\ + ,\"system\":\"aggregatetimeestimate\"}},{\"id\":\"resolutiondate\",\"name\"\ + :\"Resolved\",\"custom\":false,\"orderable\":false,\"navigable\":true,\"searchable\"\ + :true,\"clauseNames\":[\"resolutiondate\",\"resolved\"],\"schema\":{\"type\"\ + :\"datetime\",\"system\":\"resolutiondate\"}},{\"id\":\"workratio\",\"name\"\ + :\"Work Ratio\",\"custom\":false,\"orderable\":false,\"navigable\":true,\"\ + searchable\":true,\"clauseNames\":[\"workratio\"],\"schema\":{\"type\":\"\ + number\",\"system\":\"workratio\"}},{\"id\":\"summary\",\"name\":\"Summary\"\ + ,\"custom\":false,\"orderable\":true,\"navigable\":true,\"searchable\":true,\"\ + clauseNames\":[\"summary\"],\"schema\":{\"type\":\"string\",\"system\":\"\ + summary\"}},{\"id\":\"lastViewed\",\"name\":\"Last Viewed\",\"custom\":false,\"\ + orderable\":false,\"navigable\":true,\"searchable\":false,\"clauseNames\"\ + :[\"lastViewed\"],\"schema\":{\"type\":\"datetime\",\"system\":\"lastViewed\"\ + }},{\"id\":\"watches\",\"name\":\"Watchers\",\"custom\":false,\"orderable\"\ + :false,\"navigable\":true,\"searchable\":false,\"clauseNames\":[\"watchers\"\ + ],\"schema\":{\"type\":\"watches\",\"system\":\"watches\"}},{\"id\":\"creator\"\ + ,\"name\":\"Creator\",\"custom\":false,\"orderable\":false,\"navigable\":true,\"\ + searchable\":true,\"clauseNames\":[\"creator\"],\"schema\":{\"type\":\"user\"\ + ,\"system\":\"creator\"}},{\"id\":\"thumbnail\",\"name\":\"Images\",\"custom\"\ + :false,\"orderable\":false,\"navigable\":true,\"searchable\":false,\"clauseNames\"\ + :[]},{\"id\":\"subtasks\",\"name\":\"Sub-Tasks\",\"custom\":false,\"orderable\"\ + :false,\"navigable\":true,\"searchable\":false,\"clauseNames\":[\"subtasks\"\ + ],\"schema\":{\"type\":\"array\",\"items\":\"issuelinks\",\"system\":\"subtasks\"\ + }},{\"id\":\"created\",\"name\":\"Created\",\"custom\":false,\"orderable\"\ + :false,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"created\"\ + ,\"createdDate\"],\"schema\":{\"type\":\"datetime\",\"system\":\"created\"\ + }},{\"id\":\"reporter\",\"name\":\"Reporter\",\"custom\":false,\"orderable\"\ + :true,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"reporter\"\ + ],\"schema\":{\"type\":\"user\",\"system\":\"reporter\"}},{\"id\":\"aggregateprogress\"\ + ,\"name\":\"\u03A3 Progress\",\"custom\":false,\"orderable\":false,\"navigable\"\ + :true,\"searchable\":false,\"clauseNames\":[],\"schema\":{\"type\":\"progress\"\ + ,\"system\":\"aggregateprogress\"}},{\"id\":\"customfield_10000\",\"name\"\ + :\"Development\",\"custom\":true,\"orderable\":true,\"navigable\":true,\"\ + searchable\":true,\"clauseNames\":[\"cf[10000]\",\"Development\"],\"schema\"\ + :{\"type\":\"any\",\"custom\":\"com.atlassian.jira.plugins.jira-development-integration-plugin:devsummary\"\ + ,\"customId\":10000}},{\"id\":\"priority\",\"name\":\"Priority\",\"custom\"\ + :false,\"orderable\":true,\"navigable\":true,\"searchable\":true,\"clauseNames\"\ + :[\"priority\"],\"schema\":{\"type\":\"priority\",\"system\":\"priority\"\ + }},{\"id\":\"customfield_10100\",\"name\":\"Sprint\",\"custom\":true,\"orderable\"\ + :true,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"cf[10100]\"\ + ,\"Sprint\"],\"schema\":{\"type\":\"array\",\"items\":\"string\",\"custom\"\ + :\"com.pyxis.greenhopper.jira:gh-sprint\",\"customId\":10100}},{\"id\":\"\ + customfield_10101\",\"name\":\"Epic Link\",\"custom\":true,\"orderable\":true,\"\ + navigable\":true,\"searchable\":true,\"clauseNames\":[\"cf[10101]\",\"Epic\ + \ Link\"],\"schema\":{\"type\":\"any\",\"custom\":\"com.pyxis.greenhopper.jira:gh-epic-link\"\ + ,\"customId\":10101}},{\"id\":\"customfield_10102\",\"name\":\"Epic Status\"\ + ,\"custom\":true,\"orderable\":true,\"navigable\":true,\"searchable\":true,\"\ + clauseNames\":[\"cf[10102]\",\"Epic Status\"],\"schema\":{\"type\":\"option\"\ + ,\"custom\":\"com.pyxis.greenhopper.jira:gh-epic-status\",\"customId\":10102}},{\"\ + id\":\"labels\",\"name\":\"Labels\",\"custom\":false,\"orderable\":true,\"\ + navigable\":true,\"searchable\":true,\"clauseNames\":[\"labels\"],\"schema\"\ + :{\"type\":\"array\",\"items\":\"string\",\"system\":\"labels\"}},{\"id\"\ + :\"customfield_10103\",\"name\":\"Epic Name\",\"custom\":true,\"orderable\"\ + :true,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"cf[10103]\"\ + ,\"Epic Name\"],\"schema\":{\"type\":\"string\",\"custom\":\"com.pyxis.greenhopper.jira:gh-epic-label\"\ + ,\"customId\":10103}},{\"id\":\"environment\",\"name\":\"Environment\",\"\ + custom\":false,\"orderable\":true,\"navigable\":true,\"searchable\":true,\"\ + clauseNames\":[\"environment\"],\"schema\":{\"type\":\"string\",\"system\"\ + :\"environment\"}},{\"id\":\"timeestimate\",\"name\":\"Remaining Estimate\"\ + ,\"custom\":false,\"orderable\":false,\"navigable\":true,\"searchable\":false,\"\ + clauseNames\":[\"remainingEstimate\",\"timeestimate\"],\"schema\":{\"type\"\ + :\"number\",\"system\":\"timeestimate\"}},{\"id\":\"aggregatetimeoriginalestimate\"\ + ,\"name\":\"\u03A3 Original Estimate\",\"custom\":false,\"orderable\":false,\"\ + navigable\":true,\"searchable\":false,\"clauseNames\":[],\"schema\":{\"type\"\ + :\"number\",\"system\":\"aggregatetimeoriginalestimate\"}},{\"id\":\"versions\"\ + ,\"name\":\"Affects Version/s\",\"custom\":false,\"orderable\":true,\"navigable\"\ + :true,\"searchable\":true,\"clauseNames\":[\"affectedVersion\"],\"schema\"\ + :{\"type\":\"array\",\"items\":\"version\",\"system\":\"versions\"}},{\"id\"\ + :\"duedate\",\"name\":\"Due Date\",\"custom\":false,\"orderable\":true,\"\ + navigable\":true,\"searchable\":true,\"clauseNames\":[\"due\",\"duedate\"\ + ],\"schema\":{\"type\":\"date\",\"system\":\"duedate\"}},{\"id\":\"progress\"\ + ,\"name\":\"Progress\",\"custom\":false,\"orderable\":false,\"navigable\"\ + :true,\"searchable\":false,\"clauseNames\":[\"progress\"],\"schema\":{\"type\"\ + :\"progress\",\"system\":\"progress\"}},{\"id\":\"comment\",\"name\":\"Comment\"\ + ,\"custom\":false,\"orderable\":true,\"navigable\":false,\"searchable\":true,\"\ + clauseNames\":[\"comment\"],\"schema\":{\"type\":\"comments-page\",\"system\"\ + :\"comment\"}},{\"id\":\"votes\",\"name\":\"Votes\",\"custom\":false,\"orderable\"\ + :false,\"navigable\":true,\"searchable\":false,\"clauseNames\":[\"votes\"\ + ],\"schema\":{\"type\":\"votes\",\"system\":\"votes\"}},{\"id\":\"issuelinks\"\ + ,\"name\":\"Linked Issues\",\"custom\":false,\"orderable\":true,\"navigable\"\ + :true,\"searchable\":true,\"clauseNames\":[],\"schema\":{\"type\":\"array\"\ + ,\"items\":\"issuelinks\",\"system\":\"issuelinks\"}},{\"id\":\"worklog\"\ + ,\"name\":\"Log Work\",\"custom\":false,\"orderable\":true,\"navigable\":false,\"\ + searchable\":true,\"clauseNames\":[],\"schema\":{\"type\":\"array\",\"items\"\ + :\"worklog\",\"system\":\"worklog\"}},{\"id\":\"assignee\",\"name\":\"Assignee\"\ + ,\"custom\":false,\"orderable\":true,\"navigable\":true,\"searchable\":true,\"\ + clauseNames\":[\"assignee\"],\"schema\":{\"type\":\"user\",\"system\":\"assignee\"\ + }},{\"id\":\"updated\",\"name\":\"Updated\",\"custom\":false,\"orderable\"\ + :false,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"updated\"\ + ,\"updatedDate\"],\"schema\":{\"type\":\"datetime\",\"system\":\"updated\"\ + }},{\"id\":\"status\",\"name\":\"Status\",\"custom\":false,\"orderable\":false,\"\ + navigable\":true,\"searchable\":true,\"clauseNames\":[\"status\"],\"schema\"\ + :{\"type\":\"status\",\"system\":\"status\"}}]"} + headers: + Cache-Control: ['no-cache, no-store, no-transform'] + Content-Security-Policy: [frame-ancestors 'self'] + Content-Type: [application/json;charset=UTF-8] + Date: ['Thu, 04 Apr 2019 10:10:48 GMT'] + Set-Cookie: [JSESSIONID=9D3075D227E22724D77E3E27C7B9A37B; Path=/; HttpOnly] + Transfer-Encoding: [chunked] + Vary: [User-Agent] + X-AREQUESTID: [610x1137x1] + X-ASEN: [SEN-L13384799] + X-ASESSIONID: [1ih6nc9] + X-AUSERNAME: [gbaconnier] + X-Content-Type-Options: [nosniff] + X-Frame-Options: [SAMEORIGIN] + X-Seraph-LoginReason: [OK] + X-XSS-Protection: [1; mode=block] + content-length: ['9557'] + status: {code: 200, message: ''} +- request: + body: null + headers: + Accept: + - !!binary | + YXBwbGljYXRpb24vanNvbiwqLio7cT0wLjk= + Accept-Encoding: + - !!binary | + Z3ppcCwgZGVmbGF0ZQ== + Cache-Control: + - !!binary | + bm8tY2FjaGU= + Connection: + - !!binary | + a2VlcC1hbGl2ZQ== + Content-Type: + - !!binary | + YXBwbGljYXRpb24vanNvbg== + Cookie: + - !!binary | + SlNFU1NJT05JRD05RDMwNzVEMjI3RTIyNzI0RDc3RTNFMjdDN0I5QTM3QjsgYXRsYXNzaWFuLnhz + cmYudG9rZW49QllHMy02U1BGLTBVTTEtMkxCT19mNGZlZDE5MDdlMjcwOGViMTYzOTAxYTQ2YjNh + MGNiODE1MjYwOWIyX2xpbg== + User-Agent: + - !!binary | + cHl0aG9uLXJlcXVlc3RzLzIuMjEuMA== + X-Atlassian-Token: + - !!binary | + bm8tY2hlY2s= + method: GET + uri: http://jira:8080/rest/api/2/issue/10001 + response: + body: {string: '{"expand":"renderedFields,names,schema,operations,editmeta,changelog,versionedRepresentations","id":"10001","self":"http://jira:8080/rest/api/2/issue/10001","key":"TEST-2","fields":{"issuetype":{"self":"http://jira:8080/rest/api/2/issuetype/10002","id":"10002","description":"A + task that needs to be done.","iconUrl":"http://jira:8080/secure/viewavatar?size=xsmall&avatarId=10318&avatarType=issuetype","name":"Task","subtask":false,"avatarId":10318},"components":[],"timespent":null,"timeoriginalestimate":null,"description":"my + task","project":{"self":"http://jira:8080/rest/api/2/project/10000","id":"10000","key":"TEST","name":"TEST","avatarUrls":{"48x48":"http://jira:8080/secure/projectavatar?avatarId=10324","24x24":"http://jira:8080/secure/projectavatar?size=small&avatarId=10324","16x16":"http://jira:8080/secure/projectavatar?size=xsmall&avatarId=10324","32x32":"http://jira:8080/secure/projectavatar?size=medium&avatarId=10324"}},"fixVersions":[],"aggregatetimespent":null,"resolution":null,"timetracking":{},"customfield_10105":"0|i00007:","attachment":[],"aggregatetimeestimate":null,"resolutiondate":null,"workratio":-1,"summary":"Task1","lastViewed":"2019-04-04T10:09:08.870+0000","watches":{"self":"http://jira:8080/rest/api/2/issue/TEST-2/watchers","watchCount":1,"isWatching":true},"creator":{"self":"http://jira:8080/rest/api/2/user?username=gbaconnier","name":"gbaconnier","key":"gbaconnier","emailAddress":"guewen.baconnier@camptocamp.com","avatarUrls":{"48x48":"https://www.gravatar.com/avatar/ad252192c3f73885676b7d2e850ad13c?d=mm&s=48","24x24":"https://www.gravatar.com/avatar/ad252192c3f73885676b7d2e850ad13c?d=mm&s=24","16x16":"https://www.gravatar.com/avatar/ad252192c3f73885676b7d2e850ad13c?d=mm&s=16","32x32":"https://www.gravatar.com/avatar/ad252192c3f73885676b7d2e850ad13c?d=mm&s=32"},"displayName":"Guewen + Baconnier","active":true,"timeZone":"GMT"},"subtasks":[{"id":"10002","key":"TEST-3","self":"http://jira:8080/rest/api/2/issue/10002","fields":{"summary":"Subtask1","status":{"self":"http://jira:8080/rest/api/2/status/10000","description":"","iconUrl":"http://jira:8080/","name":"To + Do","id":"10000","statusCategory":{"self":"http://jira:8080/rest/api/2/statuscategory/2","id":2,"key":"new","colorName":"blue-gray","name":"To + Do"}},"priority":{"self":"http://jira:8080/rest/api/2/priority/3","iconUrl":"http://jira:8080/images/icons/priorities/medium.svg","name":"Medium","id":"3"},"issuetype":{"self":"http://jira:8080/rest/api/2/issuetype/10003","id":"10003","description":"The + sub-task of the issue","iconUrl":"http://jira:8080/secure/viewavatar?size=xsmall&avatarId=10316&avatarType=issuetype","name":"Sub-task","subtask":true,"avatarId":10316}}}],"created":"2019-04-04T09:58:51.611+0000","reporter":{"self":"http://jira:8080/rest/api/2/user?username=gbaconnier","name":"gbaconnier","key":"gbaconnier","emailAddress":"guewen.baconnier@camptocamp.com","avatarUrls":{"48x48":"https://www.gravatar.com/avatar/ad252192c3f73885676b7d2e850ad13c?d=mm&s=48","24x24":"https://www.gravatar.com/avatar/ad252192c3f73885676b7d2e850ad13c?d=mm&s=24","16x16":"https://www.gravatar.com/avatar/ad252192c3f73885676b7d2e850ad13c?d=mm&s=16","32x32":"https://www.gravatar.com/avatar/ad252192c3f73885676b7d2e850ad13c?d=mm&s=32"},"displayName":"Guewen + Baconnier","active":true,"timeZone":"GMT"},"customfield_10000":"{summaryBean=com.atlassian.jira.plugin.devstatus.rest.SummaryBean@72a6eb58[summary={pullrequest=com.atlassian.jira.plugin.devstatus.rest.SummaryItemBean@2a6cfaf5[overall=PullRequestOverallBean{stateCount=0, + state=''OPEN'', details=PullRequestOverallDetails{openCount=0, mergedCount=0, + declinedCount=0}},byInstanceType={}], build=com.atlassian.jira.plugin.devstatus.rest.SummaryItemBean@77430ae1[overall=com.atlassian.jira.plugin.devstatus.summary.beans.BuildOverallBean@3a35edae[failedBuildCount=0,successfulBuildCount=0,unknownBuildCount=0,count=0,lastUpdated=,lastUpdatedTimestamp=],byInstanceType={}], + review=com.atlassian.jira.plugin.devstatus.rest.SummaryItemBean@288facef[overall=com.atlassian.jira.plugin.devstatus.summary.beans.ReviewsOverallBean@4ccfbe00[stateCount=0,state=,dueDate=,overDue=false,count=0,lastUpdated=,lastUpdatedTimestamp=],byInstanceType={}], + deployment-environment=com.atlassian.jira.plugin.devstatus.rest.SummaryItemBean@23b4b17a[overall=com.atlassian.jira.plugin.devstatus.summary.beans.DeploymentOverallBean@36f9603f[topEnvironments=[],showProjects=false,successfulCount=0,count=0,lastUpdated=,lastUpdatedTimestamp=],byInstanceType={}], + repository=com.atlassian.jira.plugin.devstatus.rest.SummaryItemBean@63c44adb[overall=com.atlassian.jira.plugin.devstatus.summary.beans.CommitOverallBean@524ac71d[count=0,lastUpdated=,lastUpdatedTimestamp=],byInstanceType={}], + branch=com.atlassian.jira.plugin.devstatus.rest.SummaryItemBean@5473834f[overall=com.atlassian.jira.plugin.devstatus.summary.beans.BranchOverallBean@29f6f966[count=0,lastUpdated=,lastUpdatedTimestamp=],byInstanceType={}]},errors=[],configErrors=[]], + devSummaryJson={\"cachedValue\":{\"errors\":[],\"configErrors\":[],\"summary\":{\"pullrequest\":{\"overall\":{\"count\":0,\"lastUpdated\":null,\"stateCount\":0,\"state\":\"OPEN\",\"details\":{\"openCount\":0,\"mergedCount\":0,\"declinedCount\":0,\"total\":0},\"open\":true},\"byInstanceType\":{}},\"build\":{\"overall\":{\"count\":0,\"lastUpdated\":null,\"failedBuildCount\":0,\"successfulBuildCount\":0,\"unknownBuildCount\":0},\"byInstanceType\":{}},\"review\":{\"overall\":{\"count\":0,\"lastUpdated\":null,\"stateCount\":0,\"state\":null,\"dueDate\":null,\"overDue\":false,\"completed\":false},\"byInstanceType\":{}},\"deployment-environment\":{\"overall\":{\"count\":0,\"lastUpdated\":null,\"topEnvironments\":[],\"showProjects\":false,\"successfulCount\":0},\"byInstanceType\":{}},\"repository\":{\"overall\":{\"count\":0,\"lastUpdated\":null},\"byInstanceType\":{}},\"branch\":{\"overall\":{\"count\":0,\"lastUpdated\":null},\"byInstanceType\":{}}}},\"isStale\":false}}","aggregateprogress":{"progress":0,"total":0},"priority":{"self":"http://jira:8080/rest/api/2/priority/3","iconUrl":"http://jira:8080/images/icons/priorities/medium.svg","name":"Medium","id":"3"},"customfield_10100":null,"customfield_10101":"TEST-1","labels":[],"environment":null,"timeestimate":null,"aggregatetimeoriginalestimate":null,"versions":[],"duedate":null,"progress":{"progress":0,"total":0},"comment":{"comments":[],"maxResults":0,"total":0,"startAt":0},"issuelinks":[],"votes":{"self":"http://jira:8080/rest/api/2/issue/TEST-2/votes","votes":0,"hasVoted":false},"worklog":{"startAt":0,"maxResults":20,"total":0,"worklogs":[]},"assignee":null,"updated":"2019-04-04T09:58:51.785+0000","status":{"self":"http://jira:8080/rest/api/2/status/10000","description":"","iconUrl":"http://jira:8080/","name":"To + Do","id":"10000","statusCategory":{"self":"http://jira:8080/rest/api/2/statuscategory/2","id":2,"key":"new","colorName":"blue-gray","name":"To + Do"}}}}'} + headers: + Cache-Control: ['no-cache, no-store, no-transform'] + Content-Security-Policy: [frame-ancestors 'self'] + Content-Type: [application/json;charset=UTF-8] + Date: ['Thu, 04 Apr 2019 10:10:48 GMT'] + Set-Cookie: [JSESSIONID=AD34FFDE9A0927E542A50820A6DE5C7B; Path=/; HttpOnly] + Transfer-Encoding: [chunked] + Vary: [User-Agent] + X-AREQUESTID: [610x1138x1] + X-ASEN: [SEN-L13384799] + X-ASESSIONID: [ehu1q] + X-AUSERNAME: [gbaconnier] + X-Content-Type-Options: [nosniff] + X-Frame-Options: [SAMEORIGIN] + X-Seraph-LoginReason: [OK] + X-XSS-Protection: [1; mode=block] + content-length: ['6988'] + status: {code: 200, message: ''} +- request: + body: null + headers: + Accept: + - !!binary | + YXBwbGljYXRpb24vanNvbiwqLio7cT0wLjk= + Accept-Encoding: + - !!binary | + Z3ppcCwgZGVmbGF0ZQ== + Cache-Control: + - !!binary | + bm8tY2FjaGU= + Connection: + - !!binary | + a2VlcC1hbGl2ZQ== + Content-Type: + - !!binary | + YXBwbGljYXRpb24vanNvbg== + User-Agent: + - !!binary | + cHl0aG9uLXJlcXVlc3RzLzIuMjEuMA== + X-Atlassian-Token: + - !!binary | + bm8tY2hlY2s= + method: GET + uri: http://jira:8080/rest/api/2/serverInfo + response: + body: {string: '{"baseUrl":"http://localhost:8080","version":"7.12.3","versionNumbers":[7,12,3],"deploymentType":"Server","buildNumber":712004,"buildDate":"2018-10-12T00:00:00.000+0000","serverTime":"2019-04-04T10:10:48.987+0000","scmInfo":"5ef91d760d7124da5ebec5c16a948a4a807698df","serverTitle":"Jira"}'} + headers: + Cache-Control: ['no-cache, no-store, no-transform'] + Content-Security-Policy: [frame-ancestors 'self'] + Content-Type: [application/json;charset=UTF-8] + Date: ['Thu, 04 Apr 2019 10:10:48 GMT'] + Set-Cookie: [JSESSIONID=6B6A2BFC868DC8BEF4D9B22E7CD97E17; Path=/; HttpOnly, + atlassian.xsrf.token=BYG3-6SPF-0UM1-2LBO_fb5e9fae4b3081dd01df28b2e50bcb19cdfe4bcd_lin; + Path=/] + Transfer-Encoding: [chunked] + Vary: [User-Agent] + X-AREQUESTID: [610x1139x1] + X-ASEN: [SEN-L13384799] + X-ASESSIONID: [cwxgwh] + X-AUSERNAME: [gbaconnier] + X-Content-Type-Options: [nosniff] + X-Frame-Options: [SAMEORIGIN] + X-Seraph-LoginReason: [OK] + X-XSS-Protection: [1; mode=block] + content-length: ['288'] + status: {code: 200, message: ''} +- request: + body: null + headers: + Accept: + - !!binary | + YXBwbGljYXRpb24vanNvbiwqLio7cT0wLjk= + Accept-Encoding: + - !!binary | + Z3ppcCwgZGVmbGF0ZQ== + Cache-Control: + - !!binary | + bm8tY2FjaGU= + Connection: + - !!binary | + a2VlcC1hbGl2ZQ== + Content-Type: + - !!binary | + YXBwbGljYXRpb24vanNvbg== + Cookie: + - !!binary | + SlNFU1NJT05JRD02QjZBMkJGQzg2OERDOEJFRjREOUIyMkU3Q0Q5N0UxNzsgYXRsYXNzaWFuLnhz + cmYudG9rZW49QllHMy02U1BGLTBVTTEtMkxCT19mYjVlOWZhZTRiMzA4MWRkMDFkZjI4YjJlNTBi + Y2IxOWNkZmU0YmNkX2xpbg== + User-Agent: + - !!binary | + cHl0aG9uLXJlcXVlc3RzLzIuMjEuMA== + X-Atlassian-Token: + - !!binary | + bm8tY2hlY2s= + method: GET + uri: http://jira:8080/rest/api/2/field + response: + body: {string: "[{\"id\":\"issuetype\",\"name\":\"Issue Type\",\"custom\":false,\"\ + orderable\":true,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"\ + issuetype\",\"type\"],\"schema\":{\"type\":\"issuetype\",\"system\":\"issuetype\"\ + }},{\"id\":\"components\",\"name\":\"Component/s\",\"custom\":false,\"orderable\"\ + :true,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"component\"\ + ],\"schema\":{\"type\":\"array\",\"items\":\"component\",\"system\":\"components\"\ + }},{\"id\":\"issuekey\",\"name\":\"Key\",\"custom\":false,\"orderable\":false,\"\ + navigable\":true,\"searchable\":false,\"clauseNames\":[\"id\",\"issue\",\"\ + issuekey\",\"key\"]},{\"id\":\"timespent\",\"name\":\"Time Spent\",\"custom\"\ + :false,\"orderable\":false,\"navigable\":true,\"searchable\":false,\"clauseNames\"\ + :[\"timespent\"],\"schema\":{\"type\":\"number\",\"system\":\"timespent\"\ + }},{\"id\":\"timeoriginalestimate\",\"name\":\"Original Estimate\",\"custom\"\ + :false,\"orderable\":false,\"navigable\":true,\"searchable\":false,\"clauseNames\"\ + :[\"originalEstimate\",\"timeoriginalestimate\"],\"schema\":{\"type\":\"number\"\ + ,\"system\":\"timeoriginalestimate\"}},{\"id\":\"project\",\"name\":\"Project\"\ + ,\"custom\":false,\"orderable\":false,\"navigable\":true,\"searchable\":true,\"\ + clauseNames\":[\"project\"],\"schema\":{\"type\":\"project\",\"system\":\"\ + project\"}},{\"id\":\"description\",\"name\":\"Description\",\"custom\":false,\"\ + orderable\":true,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"\ + description\"],\"schema\":{\"type\":\"string\",\"system\":\"description\"\ + }},{\"id\":\"fixVersions\",\"name\":\"Fix Version/s\",\"custom\":false,\"\ + orderable\":true,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"\ + fixVersion\"],\"schema\":{\"type\":\"array\",\"items\":\"version\",\"system\"\ + :\"fixVersions\"}},{\"id\":\"aggregatetimespent\",\"name\":\"\u03A3 Time Spent\"\ + ,\"custom\":false,\"orderable\":false,\"navigable\":true,\"searchable\":false,\"\ + clauseNames\":[],\"schema\":{\"type\":\"number\",\"system\":\"aggregatetimespent\"\ + }},{\"id\":\"resolution\",\"name\":\"Resolution\",\"custom\":false,\"orderable\"\ + :true,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"resolution\"\ + ],\"schema\":{\"type\":\"resolution\",\"system\":\"resolution\"}},{\"id\"\ + :\"timetracking\",\"name\":\"Time Tracking\",\"custom\":false,\"orderable\"\ + :true,\"navigable\":false,\"searchable\":true,\"clauseNames\":[],\"schema\"\ + :{\"type\":\"timetracking\",\"system\":\"timetracking\"}},{\"id\":\"customfield_10104\"\ + ,\"name\":\"Epic Color\",\"custom\":true,\"orderable\":true,\"navigable\"\ + :true,\"searchable\":true,\"clauseNames\":[\"cf[10104]\",\"Epic Color\"],\"\ + schema\":{\"type\":\"string\",\"custom\":\"com.pyxis.greenhopper.jira:gh-epic-color\"\ + ,\"customId\":10104}},{\"id\":\"customfield_10105\",\"name\":\"Rank\",\"custom\"\ + :true,\"orderable\":true,\"navigable\":true,\"searchable\":true,\"clauseNames\"\ + :[\"cf[10105]\",\"Rank\"],\"schema\":{\"type\":\"any\",\"custom\":\"com.pyxis.greenhopper.jira:gh-lexo-rank\"\ + ,\"customId\":10105}},{\"id\":\"security\",\"name\":\"Security Level\",\"\ + custom\":false,\"orderable\":true,\"navigable\":true,\"searchable\":true,\"\ + clauseNames\":[\"level\"],\"schema\":{\"type\":\"securitylevel\",\"system\"\ + :\"security\"}},{\"id\":\"customfield_10106\",\"name\":\"Story Points\",\"\ + custom\":true,\"orderable\":true,\"navigable\":true,\"searchable\":true,\"\ + clauseNames\":[\"cf[10106]\",\"Story Points\"],\"schema\":{\"type\":\"number\"\ + ,\"custom\":\"com.atlassian.jira.plugin.system.customfieldtypes:float\",\"\ + customId\":10106}},{\"id\":\"attachment\",\"name\":\"Attachment\",\"custom\"\ + :false,\"orderable\":true,\"navigable\":false,\"searchable\":true,\"clauseNames\"\ + :[\"attachments\"],\"schema\":{\"type\":\"array\",\"items\":\"attachment\"\ + ,\"system\":\"attachment\"}},{\"id\":\"aggregatetimeestimate\",\"name\":\"\ + \u03A3 Remaining Estimate\",\"custom\":false,\"orderable\":false,\"navigable\"\ + :true,\"searchable\":false,\"clauseNames\":[],\"schema\":{\"type\":\"number\"\ + ,\"system\":\"aggregatetimeestimate\"}},{\"id\":\"resolutiondate\",\"name\"\ + :\"Resolved\",\"custom\":false,\"orderable\":false,\"navigable\":true,\"searchable\"\ + :true,\"clauseNames\":[\"resolutiondate\",\"resolved\"],\"schema\":{\"type\"\ + :\"datetime\",\"system\":\"resolutiondate\"}},{\"id\":\"workratio\",\"name\"\ + :\"Work Ratio\",\"custom\":false,\"orderable\":false,\"navigable\":true,\"\ + searchable\":true,\"clauseNames\":[\"workratio\"],\"schema\":{\"type\":\"\ + number\",\"system\":\"workratio\"}},{\"id\":\"summary\",\"name\":\"Summary\"\ + ,\"custom\":false,\"orderable\":true,\"navigable\":true,\"searchable\":true,\"\ + clauseNames\":[\"summary\"],\"schema\":{\"type\":\"string\",\"system\":\"\ + summary\"}},{\"id\":\"lastViewed\",\"name\":\"Last Viewed\",\"custom\":false,\"\ + orderable\":false,\"navigable\":true,\"searchable\":false,\"clauseNames\"\ + :[\"lastViewed\"],\"schema\":{\"type\":\"datetime\",\"system\":\"lastViewed\"\ + }},{\"id\":\"watches\",\"name\":\"Watchers\",\"custom\":false,\"orderable\"\ + :false,\"navigable\":true,\"searchable\":false,\"clauseNames\":[\"watchers\"\ + ],\"schema\":{\"type\":\"watches\",\"system\":\"watches\"}},{\"id\":\"creator\"\ + ,\"name\":\"Creator\",\"custom\":false,\"orderable\":false,\"navigable\":true,\"\ + searchable\":true,\"clauseNames\":[\"creator\"],\"schema\":{\"type\":\"user\"\ + ,\"system\":\"creator\"}},{\"id\":\"thumbnail\",\"name\":\"Images\",\"custom\"\ + :false,\"orderable\":false,\"navigable\":true,\"searchable\":false,\"clauseNames\"\ + :[]},{\"id\":\"subtasks\",\"name\":\"Sub-Tasks\",\"custom\":false,\"orderable\"\ + :false,\"navigable\":true,\"searchable\":false,\"clauseNames\":[\"subtasks\"\ + ],\"schema\":{\"type\":\"array\",\"items\":\"issuelinks\",\"system\":\"subtasks\"\ + }},{\"id\":\"created\",\"name\":\"Created\",\"custom\":false,\"orderable\"\ + :false,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"created\"\ + ,\"createdDate\"],\"schema\":{\"type\":\"datetime\",\"system\":\"created\"\ + }},{\"id\":\"reporter\",\"name\":\"Reporter\",\"custom\":false,\"orderable\"\ + :true,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"reporter\"\ + ],\"schema\":{\"type\":\"user\",\"system\":\"reporter\"}},{\"id\":\"aggregateprogress\"\ + ,\"name\":\"\u03A3 Progress\",\"custom\":false,\"orderable\":false,\"navigable\"\ + :true,\"searchable\":false,\"clauseNames\":[],\"schema\":{\"type\":\"progress\"\ + ,\"system\":\"aggregateprogress\"}},{\"id\":\"customfield_10000\",\"name\"\ + :\"Development\",\"custom\":true,\"orderable\":true,\"navigable\":true,\"\ + searchable\":true,\"clauseNames\":[\"cf[10000]\",\"Development\"],\"schema\"\ + :{\"type\":\"any\",\"custom\":\"com.atlassian.jira.plugins.jira-development-integration-plugin:devsummary\"\ + ,\"customId\":10000}},{\"id\":\"priority\",\"name\":\"Priority\",\"custom\"\ + :false,\"orderable\":true,\"navigable\":true,\"searchable\":true,\"clauseNames\"\ + :[\"priority\"],\"schema\":{\"type\":\"priority\",\"system\":\"priority\"\ + }},{\"id\":\"customfield_10100\",\"name\":\"Sprint\",\"custom\":true,\"orderable\"\ + :true,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"cf[10100]\"\ + ,\"Sprint\"],\"schema\":{\"type\":\"array\",\"items\":\"string\",\"custom\"\ + :\"com.pyxis.greenhopper.jira:gh-sprint\",\"customId\":10100}},{\"id\":\"\ + customfield_10101\",\"name\":\"Epic Link\",\"custom\":true,\"orderable\":true,\"\ + navigable\":true,\"searchable\":true,\"clauseNames\":[\"cf[10101]\",\"Epic\ + \ Link\"],\"schema\":{\"type\":\"any\",\"custom\":\"com.pyxis.greenhopper.jira:gh-epic-link\"\ + ,\"customId\":10101}},{\"id\":\"customfield_10102\",\"name\":\"Epic Status\"\ + ,\"custom\":true,\"orderable\":true,\"navigable\":true,\"searchable\":true,\"\ + clauseNames\":[\"cf[10102]\",\"Epic Status\"],\"schema\":{\"type\":\"option\"\ + ,\"custom\":\"com.pyxis.greenhopper.jira:gh-epic-status\",\"customId\":10102}},{\"\ + id\":\"labels\",\"name\":\"Labels\",\"custom\":false,\"orderable\":true,\"\ + navigable\":true,\"searchable\":true,\"clauseNames\":[\"labels\"],\"schema\"\ + :{\"type\":\"array\",\"items\":\"string\",\"system\":\"labels\"}},{\"id\"\ + :\"customfield_10103\",\"name\":\"Epic Name\",\"custom\":true,\"orderable\"\ + :true,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"cf[10103]\"\ + ,\"Epic Name\"],\"schema\":{\"type\":\"string\",\"custom\":\"com.pyxis.greenhopper.jira:gh-epic-label\"\ + ,\"customId\":10103}},{\"id\":\"environment\",\"name\":\"Environment\",\"\ + custom\":false,\"orderable\":true,\"navigable\":true,\"searchable\":true,\"\ + clauseNames\":[\"environment\"],\"schema\":{\"type\":\"string\",\"system\"\ + :\"environment\"}},{\"id\":\"timeestimate\",\"name\":\"Remaining Estimate\"\ + ,\"custom\":false,\"orderable\":false,\"navigable\":true,\"searchable\":false,\"\ + clauseNames\":[\"remainingEstimate\",\"timeestimate\"],\"schema\":{\"type\"\ + :\"number\",\"system\":\"timeestimate\"}},{\"id\":\"aggregatetimeoriginalestimate\"\ + ,\"name\":\"\u03A3 Original Estimate\",\"custom\":false,\"orderable\":false,\"\ + navigable\":true,\"searchable\":false,\"clauseNames\":[],\"schema\":{\"type\"\ + :\"number\",\"system\":\"aggregatetimeoriginalestimate\"}},{\"id\":\"versions\"\ + ,\"name\":\"Affects Version/s\",\"custom\":false,\"orderable\":true,\"navigable\"\ + :true,\"searchable\":true,\"clauseNames\":[\"affectedVersion\"],\"schema\"\ + :{\"type\":\"array\",\"items\":\"version\",\"system\":\"versions\"}},{\"id\"\ + :\"duedate\",\"name\":\"Due Date\",\"custom\":false,\"orderable\":true,\"\ + navigable\":true,\"searchable\":true,\"clauseNames\":[\"due\",\"duedate\"\ + ],\"schema\":{\"type\":\"date\",\"system\":\"duedate\"}},{\"id\":\"progress\"\ + ,\"name\":\"Progress\",\"custom\":false,\"orderable\":false,\"navigable\"\ + :true,\"searchable\":false,\"clauseNames\":[\"progress\"],\"schema\":{\"type\"\ + :\"progress\",\"system\":\"progress\"}},{\"id\":\"comment\",\"name\":\"Comment\"\ + ,\"custom\":false,\"orderable\":true,\"navigable\":false,\"searchable\":true,\"\ + clauseNames\":[\"comment\"],\"schema\":{\"type\":\"comments-page\",\"system\"\ + :\"comment\"}},{\"id\":\"votes\",\"name\":\"Votes\",\"custom\":false,\"orderable\"\ + :false,\"navigable\":true,\"searchable\":false,\"clauseNames\":[\"votes\"\ + ],\"schema\":{\"type\":\"votes\",\"system\":\"votes\"}},{\"id\":\"issuelinks\"\ + ,\"name\":\"Linked Issues\",\"custom\":false,\"orderable\":true,\"navigable\"\ + :true,\"searchable\":true,\"clauseNames\":[],\"schema\":{\"type\":\"array\"\ + ,\"items\":\"issuelinks\",\"system\":\"issuelinks\"}},{\"id\":\"worklog\"\ + ,\"name\":\"Log Work\",\"custom\":false,\"orderable\":true,\"navigable\":false,\"\ + searchable\":true,\"clauseNames\":[],\"schema\":{\"type\":\"array\",\"items\"\ + :\"worklog\",\"system\":\"worklog\"}},{\"id\":\"assignee\",\"name\":\"Assignee\"\ + ,\"custom\":false,\"orderable\":true,\"navigable\":true,\"searchable\":true,\"\ + clauseNames\":[\"assignee\"],\"schema\":{\"type\":\"user\",\"system\":\"assignee\"\ + }},{\"id\":\"updated\",\"name\":\"Updated\",\"custom\":false,\"orderable\"\ + :false,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"updated\"\ + ,\"updatedDate\"],\"schema\":{\"type\":\"datetime\",\"system\":\"updated\"\ + }},{\"id\":\"status\",\"name\":\"Status\",\"custom\":false,\"orderable\":false,\"\ + navigable\":true,\"searchable\":true,\"clauseNames\":[\"status\"],\"schema\"\ + :{\"type\":\"status\",\"system\":\"status\"}}]"} + headers: + Cache-Control: ['no-cache, no-store, no-transform'] + Content-Security-Policy: [frame-ancestors 'self'] + Content-Type: [application/json;charset=UTF-8] + Date: ['Thu, 04 Apr 2019 10:10:48 GMT'] + Set-Cookie: [JSESSIONID=B3514D1617BDDB5E7C48069C0CB77C44; Path=/; HttpOnly] + Transfer-Encoding: [chunked] + Vary: [User-Agent] + X-AREQUESTID: [610x1140x1] + X-ASEN: [SEN-L13384799] + X-ASESSIONID: [gkrqf8] + X-AUSERNAME: [gbaconnier] + X-Content-Type-Options: [nosniff] + X-Frame-Options: [SAMEORIGIN] + X-Seraph-LoginReason: [OK] + X-XSS-Protection: [1; mode=block] + content-length: ['9557'] + status: {code: 200, message: ''} +- request: + body: null + headers: + Accept: + - !!binary | + YXBwbGljYXRpb24vanNvbiwqLio7cT0wLjk= + Accept-Encoding: + - !!binary | + Z3ppcCwgZGVmbGF0ZQ== + Cache-Control: + - !!binary | + bm8tY2FjaGU= + Connection: + - !!binary | + a2VlcC1hbGl2ZQ== + Content-Type: + - !!binary | + YXBwbGljYXRpb24vanNvbg== + Cookie: + - !!binary | + SlNFU1NJT05JRD1CMzUxNEQxNjE3QkREQjVFN0M0ODA2OUMwQ0I3N0M0NDsgYXRsYXNzaWFuLnhz + cmYudG9rZW49QllHMy02U1BGLTBVTTEtMkxCT19mYjVlOWZhZTRiMzA4MWRkMDFkZjI4YjJlNTBi + Y2IxOWNkZmU0YmNkX2xpbg== + User-Agent: + - !!binary | + cHl0aG9uLXJlcXVlc3RzLzIuMjEuMA== + X-Atlassian-Token: + - !!binary | + bm8tY2hlY2s= + method: GET + uri: http://jira:8080/rest/api/2/issue/TEST-1 + response: + body: {string: '{"expand":"renderedFields,names,schema,operations,editmeta,changelog,versionedRepresentations","id":"10000","self":"http://jira:8080/rest/api/2/issue/10000","key":"TEST-1","fields":{"issuetype":{"self":"http://jira:8080/rest/api/2/issuetype/10000","id":"10000","description":"Created + by Jira Software - do not edit or delete. Issue type for a big user story + that needs to be broken down.","iconUrl":"http://jira:8080/images/icons/issuetypes/epic.svg","name":"Epic","subtask":false},"components":[],"timespent":null,"timeoriginalestimate":null,"description":null,"project":{"self":"http://jira:8080/rest/api/2/project/10000","id":"10000","key":"TEST","name":"TEST","avatarUrls":{"48x48":"http://jira:8080/secure/projectavatar?avatarId=10324","24x24":"http://jira:8080/secure/projectavatar?size=small&avatarId=10324","16x16":"http://jira:8080/secure/projectavatar?size=xsmall&avatarId=10324","32x32":"http://jira:8080/secure/projectavatar?size=medium&avatarId=10324"}},"fixVersions":[],"aggregatetimespent":null,"resolution":null,"timetracking":{},"customfield_10104":"ghx-label-1","customfield_10105":"0|hzzzzz:","customfield_10106":null,"attachment":[],"aggregatetimeestimate":null,"resolutiondate":null,"workratio":-1,"summary":"Epic1","lastViewed":"2019-04-04T09:31:34.589+0000","watches":{"self":"http://jira:8080/rest/api/2/issue/TEST-1/watchers","watchCount":1,"isWatching":true},"creator":{"self":"http://jira:8080/rest/api/2/user?username=gbaconnier","name":"gbaconnier","key":"gbaconnier","emailAddress":"guewen.baconnier@camptocamp.com","avatarUrls":{"48x48":"https://www.gravatar.com/avatar/ad252192c3f73885676b7d2e850ad13c?d=mm&s=48","24x24":"https://www.gravatar.com/avatar/ad252192c3f73885676b7d2e850ad13c?d=mm&s=24","16x16":"https://www.gravatar.com/avatar/ad252192c3f73885676b7d2e850ad13c?d=mm&s=16","32x32":"https://www.gravatar.com/avatar/ad252192c3f73885676b7d2e850ad13c?d=mm&s=32"},"displayName":"Guewen + Baconnier","active":true,"timeZone":"GMT"},"subtasks":[],"created":"2019-04-04T09:31:27.779+0000","reporter":{"self":"http://jira:8080/rest/api/2/user?username=gbaconnier","name":"gbaconnier","key":"gbaconnier","emailAddress":"guewen.baconnier@camptocamp.com","avatarUrls":{"48x48":"https://www.gravatar.com/avatar/ad252192c3f73885676b7d2e850ad13c?d=mm&s=48","24x24":"https://www.gravatar.com/avatar/ad252192c3f73885676b7d2e850ad13c?d=mm&s=24","16x16":"https://www.gravatar.com/avatar/ad252192c3f73885676b7d2e850ad13c?d=mm&s=16","32x32":"https://www.gravatar.com/avatar/ad252192c3f73885676b7d2e850ad13c?d=mm&s=32"},"displayName":"Guewen + Baconnier","active":true,"timeZone":"GMT"},"customfield_10000":"{summaryBean=com.atlassian.jira.plugin.devstatus.rest.SummaryBean@658f590[summary={pullrequest=com.atlassian.jira.plugin.devstatus.rest.SummaryItemBean@1cb0ec8a[overall=PullRequestOverallBean{stateCount=0, + state=''OPEN'', details=PullRequestOverallDetails{openCount=0, mergedCount=0, + declinedCount=0}},byInstanceType={}], build=com.atlassian.jira.plugin.devstatus.rest.SummaryItemBean@6296b8d6[overall=com.atlassian.jira.plugin.devstatus.summary.beans.BuildOverallBean@79d9dcb2[failedBuildCount=0,successfulBuildCount=0,unknownBuildCount=0,count=0,lastUpdated=,lastUpdatedTimestamp=],byInstanceType={}], + review=com.atlassian.jira.plugin.devstatus.rest.SummaryItemBean@5a9370b0[overall=com.atlassian.jira.plugin.devstatus.summary.beans.ReviewsOverallBean@60602730[stateCount=0,state=,dueDate=,overDue=false,count=0,lastUpdated=,lastUpdatedTimestamp=],byInstanceType={}], + deployment-environment=com.atlassian.jira.plugin.devstatus.rest.SummaryItemBean@37e763e2[overall=com.atlassian.jira.plugin.devstatus.summary.beans.DeploymentOverallBean@ec15e0[topEnvironments=[],showProjects=false,successfulCount=0,count=0,lastUpdated=,lastUpdatedTimestamp=],byInstanceType={}], + repository=com.atlassian.jira.plugin.devstatus.rest.SummaryItemBean@41bbf3bd[overall=com.atlassian.jira.plugin.devstatus.summary.beans.CommitOverallBean@2003051d[count=0,lastUpdated=,lastUpdatedTimestamp=],byInstanceType={}], + branch=com.atlassian.jira.plugin.devstatus.rest.SummaryItemBean@2cf79a8c[overall=com.atlassian.jira.plugin.devstatus.summary.beans.BranchOverallBean@4cd4d9e7[count=0,lastUpdated=,lastUpdatedTimestamp=],byInstanceType={}]},errors=[],configErrors=[]], + devSummaryJson={\"cachedValue\":{\"errors\":[],\"configErrors\":[],\"summary\":{\"pullrequest\":{\"overall\":{\"count\":0,\"lastUpdated\":null,\"stateCount\":0,\"state\":\"OPEN\",\"details\":{\"openCount\":0,\"mergedCount\":0,\"declinedCount\":0,\"total\":0},\"open\":true},\"byInstanceType\":{}},\"build\":{\"overall\":{\"count\":0,\"lastUpdated\":null,\"failedBuildCount\":0,\"successfulBuildCount\":0,\"unknownBuildCount\":0},\"byInstanceType\":{}},\"review\":{\"overall\":{\"count\":0,\"lastUpdated\":null,\"stateCount\":0,\"state\":null,\"dueDate\":null,\"overDue\":false,\"completed\":false},\"byInstanceType\":{}},\"deployment-environment\":{\"overall\":{\"count\":0,\"lastUpdated\":null,\"topEnvironments\":[],\"showProjects\":false,\"successfulCount\":0},\"byInstanceType\":{}},\"repository\":{\"overall\":{\"count\":0,\"lastUpdated\":null},\"byInstanceType\":{}},\"branch\":{\"overall\":{\"count\":0,\"lastUpdated\":null},\"byInstanceType\":{}}}},\"isStale\":false}}","aggregateprogress":{"progress":0,"total":0},"priority":{"self":"http://jira:8080/rest/api/2/priority/3","iconUrl":"http://jira:8080/images/icons/priorities/medium.svg","name":"Medium","id":"3"},"customfield_10100":null,"customfield_10101":null,"customfield_10102":{"self":"http://jira:8080/rest/api/2/customFieldOption/10000","value":"To + Do","id":"10000"},"labels":[],"customfield_10103":"Epic1","environment":null,"timeestimate":null,"aggregatetimeoriginalestimate":null,"versions":[],"duedate":null,"progress":{"progress":0,"total":0},"comment":{"comments":[],"maxResults":0,"total":0,"startAt":0},"issuelinks":[],"votes":{"self":"http://jira:8080/rest/api/2/issue/TEST-1/votes","votes":0,"hasVoted":false},"worklog":{"startAt":0,"maxResults":20,"total":0,"worklogs":[]},"assignee":null,"updated":"2019-04-04T09:58:51.800+0000","status":{"self":"http://jira:8080/rest/api/2/status/10000","description":"","iconUrl":"http://jira:8080/","name":"To + Do","id":"10000","statusCategory":{"self":"http://jira:8080/rest/api/2/statuscategory/2","id":2,"key":"new","colorName":"blue-gray","name":"To + Do"}}}}'} + headers: + Cache-Control: ['no-cache, no-store, no-transform'] + Content-Security-Policy: [frame-ancestors 'self'] + Content-Type: [application/json;charset=UTF-8] + Date: ['Thu, 04 Apr 2019 10:10:48 GMT'] + Set-Cookie: [JSESSIONID=FA5622CEC4D5B9481C81AA8AEA78E8EB; Path=/; HttpOnly] + Transfer-Encoding: [chunked] + Vary: [User-Agent] + X-AREQUESTID: [610x1141x1] + X-ASEN: [SEN-L13384799] + X-ASESSIONID: [1kkgrsr] + X-AUSERNAME: [gbaconnier] + X-Content-Type-Options: [nosniff] + X-Frame-Options: [SAMEORIGIN] + X-Seraph-LoginReason: [OK] + X-XSS-Protection: [1; mode=block] + content-length: ['6413'] + status: {code: 200, message: ''} +version: 1 diff --git a/connector_jira/tests/fixtures/cassettes/test_import_task_type_not_synced.yaml b/connector_jira/tests/fixtures/cassettes/test_import_task_type_not_synced.yaml new file mode 100644 index 000000000..247961ec2 --- /dev/null +++ b/connector_jira/tests/fixtures/cassettes/test_import_task_type_not_synced.yaml @@ -0,0 +1,308 @@ +interactions: +- request: + body: null + headers: + Accept: + - !!binary | + YXBwbGljYXRpb24vanNvbiwqLio7cT0wLjk= + Accept-Encoding: + - !!binary | + Z3ppcCwgZGVmbGF0ZQ== + Cache-Control: + - !!binary | + bm8tY2FjaGU= + Connection: + - !!binary | + a2VlcC1hbGl2ZQ== + Content-Type: + - !!binary | + YXBwbGljYXRpb24vanNvbg== + User-Agent: + - !!binary | + cHl0aG9uLXJlcXVlc3RzLzIuMjEuMA== + X-Atlassian-Token: + - !!binary | + bm8tY2hlY2s= + method: GET + uri: http://jira:8080/rest/api/2/serverInfo + response: + body: {string: '{"baseUrl":"http://localhost:8080","version":"7.12.3","versionNumbers":[7,12,3],"deploymentType":"Server","buildNumber":712004,"buildDate":"2018-10-12T00:00:00.000+0000","serverTime":"2019-04-04T09:52:00.339+0000","scmInfo":"5ef91d760d7124da5ebec5c16a948a4a807698df","serverTitle":"Jira"}'} + headers: + Cache-Control: ['no-cache, no-store, no-transform'] + Content-Security-Policy: [frame-ancestors 'self'] + Content-Type: [application/json;charset=UTF-8] + Date: ['Thu, 04 Apr 2019 09:52:00 GMT'] + Set-Cookie: [JSESSIONID=0178D1441E3D3F41729C0575D2ED3BEB; Path=/; HttpOnly, + atlassian.xsrf.token=BYG3-6SPF-0UM1-2LBO_fe4f608670710bd6d97ccacdb46168a9129036c3_lin; + Path=/] + Transfer-Encoding: [chunked] + Vary: [User-Agent] + X-AREQUESTID: [592x1052x1] + X-ASEN: [SEN-L13384799] + X-ASESSIONID: [rseuf8] + X-AUSERNAME: [gbaconnier] + X-Content-Type-Options: [nosniff] + X-Frame-Options: [SAMEORIGIN] + X-Seraph-LoginReason: [OK] + X-XSS-Protection: [1; mode=block] + content-length: ['288'] + status: {code: 200, message: ''} +- request: + body: null + headers: + Accept: + - !!binary | + YXBwbGljYXRpb24vanNvbiwqLio7cT0wLjk= + Accept-Encoding: + - !!binary | + Z3ppcCwgZGVmbGF0ZQ== + Cache-Control: + - !!binary | + bm8tY2FjaGU= + Connection: + - !!binary | + a2VlcC1hbGl2ZQ== + Content-Type: + - !!binary | + YXBwbGljYXRpb24vanNvbg== + Cookie: + - !!binary | + YXRsYXNzaWFuLnhzcmYudG9rZW49QllHMy02U1BGLTBVTTEtMkxCT19mZTRmNjA4NjcwNzEwYmQ2 + ZDk3Y2NhY2RiNDYxNjhhOTEyOTAzNmMzX2xpbjsgSlNFU1NJT05JRD0wMTc4RDE0NDFFM0QzRjQx + NzI5QzA1NzVEMkVEM0JFQg== + User-Agent: + - !!binary | + cHl0aG9uLXJlcXVlc3RzLzIuMjEuMA== + X-Atlassian-Token: + - !!binary | + bm8tY2hlY2s= + method: GET + uri: http://jira:8080/rest/api/2/field + response: + body: {string: "[{\"id\":\"issuetype\",\"name\":\"Issue Type\",\"custom\":false,\"\ + orderable\":true,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"\ + issuetype\",\"type\"],\"schema\":{\"type\":\"issuetype\",\"system\":\"issuetype\"\ + }},{\"id\":\"components\",\"name\":\"Component/s\",\"custom\":false,\"orderable\"\ + :true,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"component\"\ + ],\"schema\":{\"type\":\"array\",\"items\":\"component\",\"system\":\"components\"\ + }},{\"id\":\"issuekey\",\"name\":\"Key\",\"custom\":false,\"orderable\":false,\"\ + navigable\":true,\"searchable\":false,\"clauseNames\":[\"id\",\"issue\",\"\ + issuekey\",\"key\"]},{\"id\":\"timespent\",\"name\":\"Time Spent\",\"custom\"\ + :false,\"orderable\":false,\"navigable\":true,\"searchable\":false,\"clauseNames\"\ + :[\"timespent\"],\"schema\":{\"type\":\"number\",\"system\":\"timespent\"\ + }},{\"id\":\"timeoriginalestimate\",\"name\":\"Original Estimate\",\"custom\"\ + :false,\"orderable\":false,\"navigable\":true,\"searchable\":false,\"clauseNames\"\ + :[\"originalEstimate\",\"timeoriginalestimate\"],\"schema\":{\"type\":\"number\"\ + ,\"system\":\"timeoriginalestimate\"}},{\"id\":\"project\",\"name\":\"Project\"\ + ,\"custom\":false,\"orderable\":false,\"navigable\":true,\"searchable\":true,\"\ + clauseNames\":[\"project\"],\"schema\":{\"type\":\"project\",\"system\":\"\ + project\"}},{\"id\":\"description\",\"name\":\"Description\",\"custom\":false,\"\ + orderable\":true,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"\ + description\"],\"schema\":{\"type\":\"string\",\"system\":\"description\"\ + }},{\"id\":\"fixVersions\",\"name\":\"Fix Version/s\",\"custom\":false,\"\ + orderable\":true,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"\ + fixVersion\"],\"schema\":{\"type\":\"array\",\"items\":\"version\",\"system\"\ + :\"fixVersions\"}},{\"id\":\"aggregatetimespent\",\"name\":\"\u03A3 Time Spent\"\ + ,\"custom\":false,\"orderable\":false,\"navigable\":true,\"searchable\":false,\"\ + clauseNames\":[],\"schema\":{\"type\":\"number\",\"system\":\"aggregatetimespent\"\ + }},{\"id\":\"resolution\",\"name\":\"Resolution\",\"custom\":false,\"orderable\"\ + :true,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"resolution\"\ + ],\"schema\":{\"type\":\"resolution\",\"system\":\"resolution\"}},{\"id\"\ + :\"timetracking\",\"name\":\"Time Tracking\",\"custom\":false,\"orderable\"\ + :true,\"navigable\":false,\"searchable\":true,\"clauseNames\":[],\"schema\"\ + :{\"type\":\"timetracking\",\"system\":\"timetracking\"}},{\"id\":\"customfield_10104\"\ + ,\"name\":\"Epic Color\",\"custom\":true,\"orderable\":true,\"navigable\"\ + :true,\"searchable\":true,\"clauseNames\":[\"cf[10104]\",\"Epic Color\"],\"\ + schema\":{\"type\":\"string\",\"custom\":\"com.pyxis.greenhopper.jira:gh-epic-color\"\ + ,\"customId\":10104}},{\"id\":\"customfield_10105\",\"name\":\"Rank\",\"custom\"\ + :true,\"orderable\":true,\"navigable\":true,\"searchable\":true,\"clauseNames\"\ + :[\"cf[10105]\",\"Rank\"],\"schema\":{\"type\":\"any\",\"custom\":\"com.pyxis.greenhopper.jira:gh-lexo-rank\"\ + ,\"customId\":10105}},{\"id\":\"security\",\"name\":\"Security Level\",\"\ + custom\":false,\"orderable\":true,\"navigable\":true,\"searchable\":true,\"\ + clauseNames\":[\"level\"],\"schema\":{\"type\":\"securitylevel\",\"system\"\ + :\"security\"}},{\"id\":\"customfield_10106\",\"name\":\"Story Points\",\"\ + custom\":true,\"orderable\":true,\"navigable\":true,\"searchable\":true,\"\ + clauseNames\":[\"cf[10106]\",\"Story Points\"],\"schema\":{\"type\":\"number\"\ + ,\"custom\":\"com.atlassian.jira.plugin.system.customfieldtypes:float\",\"\ + customId\":10106}},{\"id\":\"attachment\",\"name\":\"Attachment\",\"custom\"\ + :false,\"orderable\":true,\"navigable\":false,\"searchable\":true,\"clauseNames\"\ + :[\"attachments\"],\"schema\":{\"type\":\"array\",\"items\":\"attachment\"\ + ,\"system\":\"attachment\"}},{\"id\":\"aggregatetimeestimate\",\"name\":\"\ + \u03A3 Remaining Estimate\",\"custom\":false,\"orderable\":false,\"navigable\"\ + :true,\"searchable\":false,\"clauseNames\":[],\"schema\":{\"type\":\"number\"\ + ,\"system\":\"aggregatetimeestimate\"}},{\"id\":\"resolutiondate\",\"name\"\ + :\"Resolved\",\"custom\":false,\"orderable\":false,\"navigable\":true,\"searchable\"\ + :true,\"clauseNames\":[\"resolutiondate\",\"resolved\"],\"schema\":{\"type\"\ + :\"datetime\",\"system\":\"resolutiondate\"}},{\"id\":\"workratio\",\"name\"\ + :\"Work Ratio\",\"custom\":false,\"orderable\":false,\"navigable\":true,\"\ + searchable\":true,\"clauseNames\":[\"workratio\"],\"schema\":{\"type\":\"\ + number\",\"system\":\"workratio\"}},{\"id\":\"summary\",\"name\":\"Summary\"\ + ,\"custom\":false,\"orderable\":true,\"navigable\":true,\"searchable\":true,\"\ + clauseNames\":[\"summary\"],\"schema\":{\"type\":\"string\",\"system\":\"\ + summary\"}},{\"id\":\"lastViewed\",\"name\":\"Last Viewed\",\"custom\":false,\"\ + orderable\":false,\"navigable\":true,\"searchable\":false,\"clauseNames\"\ + :[\"lastViewed\"],\"schema\":{\"type\":\"datetime\",\"system\":\"lastViewed\"\ + }},{\"id\":\"watches\",\"name\":\"Watchers\",\"custom\":false,\"orderable\"\ + :false,\"navigable\":true,\"searchable\":false,\"clauseNames\":[\"watchers\"\ + ],\"schema\":{\"type\":\"watches\",\"system\":\"watches\"}},{\"id\":\"creator\"\ + ,\"name\":\"Creator\",\"custom\":false,\"orderable\":false,\"navigable\":true,\"\ + searchable\":true,\"clauseNames\":[\"creator\"],\"schema\":{\"type\":\"user\"\ + ,\"system\":\"creator\"}},{\"id\":\"thumbnail\",\"name\":\"Images\",\"custom\"\ + :false,\"orderable\":false,\"navigable\":true,\"searchable\":false,\"clauseNames\"\ + :[]},{\"id\":\"subtasks\",\"name\":\"Sub-Tasks\",\"custom\":false,\"orderable\"\ + :false,\"navigable\":true,\"searchable\":false,\"clauseNames\":[\"subtasks\"\ + ],\"schema\":{\"type\":\"array\",\"items\":\"issuelinks\",\"system\":\"subtasks\"\ + }},{\"id\":\"created\",\"name\":\"Created\",\"custom\":false,\"orderable\"\ + :false,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"created\"\ + ,\"createdDate\"],\"schema\":{\"type\":\"datetime\",\"system\":\"created\"\ + }},{\"id\":\"reporter\",\"name\":\"Reporter\",\"custom\":false,\"orderable\"\ + :true,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"reporter\"\ + ],\"schema\":{\"type\":\"user\",\"system\":\"reporter\"}},{\"id\":\"aggregateprogress\"\ + ,\"name\":\"\u03A3 Progress\",\"custom\":false,\"orderable\":false,\"navigable\"\ + :true,\"searchable\":false,\"clauseNames\":[],\"schema\":{\"type\":\"progress\"\ + ,\"system\":\"aggregateprogress\"}},{\"id\":\"customfield_10000\",\"name\"\ + :\"Development\",\"custom\":true,\"orderable\":true,\"navigable\":true,\"\ + searchable\":true,\"clauseNames\":[\"cf[10000]\",\"Development\"],\"schema\"\ + :{\"type\":\"any\",\"custom\":\"com.atlassian.jira.plugins.jira-development-integration-plugin:devsummary\"\ + ,\"customId\":10000}},{\"id\":\"priority\",\"name\":\"Priority\",\"custom\"\ + :false,\"orderable\":true,\"navigable\":true,\"searchable\":true,\"clauseNames\"\ + :[\"priority\"],\"schema\":{\"type\":\"priority\",\"system\":\"priority\"\ + }},{\"id\":\"customfield_10100\",\"name\":\"Sprint\",\"custom\":true,\"orderable\"\ + :true,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"cf[10100]\"\ + ,\"Sprint\"],\"schema\":{\"type\":\"array\",\"items\":\"string\",\"custom\"\ + :\"com.pyxis.greenhopper.jira:gh-sprint\",\"customId\":10100}},{\"id\":\"\ + customfield_10101\",\"name\":\"Epic Link\",\"custom\":true,\"orderable\":true,\"\ + navigable\":true,\"searchable\":true,\"clauseNames\":[\"cf[10101]\",\"Epic\ + \ Link\"],\"schema\":{\"type\":\"any\",\"custom\":\"com.pyxis.greenhopper.jira:gh-epic-link\"\ + ,\"customId\":10101}},{\"id\":\"customfield_10102\",\"name\":\"Epic Status\"\ + ,\"custom\":true,\"orderable\":true,\"navigable\":true,\"searchable\":true,\"\ + clauseNames\":[\"cf[10102]\",\"Epic Status\"],\"schema\":{\"type\":\"option\"\ + ,\"custom\":\"com.pyxis.greenhopper.jira:gh-epic-status\",\"customId\":10102}},{\"\ + id\":\"labels\",\"name\":\"Labels\",\"custom\":false,\"orderable\":true,\"\ + navigable\":true,\"searchable\":true,\"clauseNames\":[\"labels\"],\"schema\"\ + :{\"type\":\"array\",\"items\":\"string\",\"system\":\"labels\"}},{\"id\"\ + :\"customfield_10103\",\"name\":\"Epic Name\",\"custom\":true,\"orderable\"\ + :true,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"cf[10103]\"\ + ,\"Epic Name\"],\"schema\":{\"type\":\"string\",\"custom\":\"com.pyxis.greenhopper.jira:gh-epic-label\"\ + ,\"customId\":10103}},{\"id\":\"environment\",\"name\":\"Environment\",\"\ + custom\":false,\"orderable\":true,\"navigable\":true,\"searchable\":true,\"\ + clauseNames\":[\"environment\"],\"schema\":{\"type\":\"string\",\"system\"\ + :\"environment\"}},{\"id\":\"timeestimate\",\"name\":\"Remaining Estimate\"\ + ,\"custom\":false,\"orderable\":false,\"navigable\":true,\"searchable\":false,\"\ + clauseNames\":[\"remainingEstimate\",\"timeestimate\"],\"schema\":{\"type\"\ + :\"number\",\"system\":\"timeestimate\"}},{\"id\":\"aggregatetimeoriginalestimate\"\ + ,\"name\":\"\u03A3 Original Estimate\",\"custom\":false,\"orderable\":false,\"\ + navigable\":true,\"searchable\":false,\"clauseNames\":[],\"schema\":{\"type\"\ + :\"number\",\"system\":\"aggregatetimeoriginalestimate\"}},{\"id\":\"versions\"\ + ,\"name\":\"Affects Version/s\",\"custom\":false,\"orderable\":true,\"navigable\"\ + :true,\"searchable\":true,\"clauseNames\":[\"affectedVersion\"],\"schema\"\ + :{\"type\":\"array\",\"items\":\"version\",\"system\":\"versions\"}},{\"id\"\ + :\"duedate\",\"name\":\"Due Date\",\"custom\":false,\"orderable\":true,\"\ + navigable\":true,\"searchable\":true,\"clauseNames\":[\"due\",\"duedate\"\ + ],\"schema\":{\"type\":\"date\",\"system\":\"duedate\"}},{\"id\":\"progress\"\ + ,\"name\":\"Progress\",\"custom\":false,\"orderable\":false,\"navigable\"\ + :true,\"searchable\":false,\"clauseNames\":[\"progress\"],\"schema\":{\"type\"\ + :\"progress\",\"system\":\"progress\"}},{\"id\":\"comment\",\"name\":\"Comment\"\ + ,\"custom\":false,\"orderable\":true,\"navigable\":false,\"searchable\":true,\"\ + clauseNames\":[\"comment\"],\"schema\":{\"type\":\"comments-page\",\"system\"\ + :\"comment\"}},{\"id\":\"votes\",\"name\":\"Votes\",\"custom\":false,\"orderable\"\ + :false,\"navigable\":true,\"searchable\":false,\"clauseNames\":[\"votes\"\ + ],\"schema\":{\"type\":\"votes\",\"system\":\"votes\"}},{\"id\":\"issuelinks\"\ + ,\"name\":\"Linked Issues\",\"custom\":false,\"orderable\":true,\"navigable\"\ + :true,\"searchable\":true,\"clauseNames\":[],\"schema\":{\"type\":\"array\"\ + ,\"items\":\"issuelinks\",\"system\":\"issuelinks\"}},{\"id\":\"worklog\"\ + ,\"name\":\"Log Work\",\"custom\":false,\"orderable\":true,\"navigable\":false,\"\ + searchable\":true,\"clauseNames\":[],\"schema\":{\"type\":\"array\",\"items\"\ + :\"worklog\",\"system\":\"worklog\"}},{\"id\":\"assignee\",\"name\":\"Assignee\"\ + ,\"custom\":false,\"orderable\":true,\"navigable\":true,\"searchable\":true,\"\ + clauseNames\":[\"assignee\"],\"schema\":{\"type\":\"user\",\"system\":\"assignee\"\ + }},{\"id\":\"updated\",\"name\":\"Updated\",\"custom\":false,\"orderable\"\ + :false,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"updated\"\ + ,\"updatedDate\"],\"schema\":{\"type\":\"datetime\",\"system\":\"updated\"\ + }},{\"id\":\"status\",\"name\":\"Status\",\"custom\":false,\"orderable\":false,\"\ + navigable\":true,\"searchable\":true,\"clauseNames\":[\"status\"],\"schema\"\ + :{\"type\":\"status\",\"system\":\"status\"}}]"} + headers: + Cache-Control: ['no-cache, no-store, no-transform'] + Content-Security-Policy: [frame-ancestors 'self'] + Content-Type: [application/json;charset=UTF-8] + Date: ['Thu, 04 Apr 2019 09:52:00 GMT'] + Set-Cookie: [JSESSIONID=5D40CB247B973A3708767FA3819F4B70; Path=/; HttpOnly] + Transfer-Encoding: [chunked] + Vary: [User-Agent] + X-AREQUESTID: [592x1053x1] + X-ASEN: [SEN-L13384799] + X-ASESSIONID: [x5synk] + X-AUSERNAME: [gbaconnier] + X-Content-Type-Options: [nosniff] + X-Frame-Options: [SAMEORIGIN] + X-Seraph-LoginReason: [OK] + X-XSS-Protection: [1; mode=block] + content-length: ['9557'] + status: {code: 200, message: ''} +- request: + body: null + headers: + Accept: + - !!binary | + YXBwbGljYXRpb24vanNvbiwqLio7cT0wLjk= + Accept-Encoding: + - !!binary | + Z3ppcCwgZGVmbGF0ZQ== + Cache-Control: + - !!binary | + bm8tY2FjaGU= + Connection: + - !!binary | + a2VlcC1hbGl2ZQ== + Content-Type: + - !!binary | + YXBwbGljYXRpb24vanNvbg== + Cookie: + - !!binary | + YXRsYXNzaWFuLnhzcmYudG9rZW49QllHMy02U1BGLTBVTTEtMkxCT19mZTRmNjA4NjcwNzEwYmQ2 + ZDk3Y2NhY2RiNDYxNjhhOTEyOTAzNmMzX2xpbjsgSlNFU1NJT05JRD01RDQwQ0IyNDdCOTczQTM3 + MDg3NjdGQTM4MTlGNEI3MA== + User-Agent: + - !!binary | + cHl0aG9uLXJlcXVlc3RzLzIuMjEuMA== + X-Atlassian-Token: + - !!binary | + bm8tY2hlY2s= + method: GET + uri: http://jira:8080/rest/api/2/issue/10000 + response: + body: {string: '{"expand":"renderedFields,names,schema,operations,editmeta,changelog,versionedRepresentations","id":"10000","self":"http://jira:8080/rest/api/2/issue/10000","key":"TEST-1","fields":{"issuetype":{"self":"http://jira:8080/rest/api/2/issuetype/10000","id":"10000","description":"Created + by Jira Software - do not edit or delete. Issue type for a big user story + that needs to be broken down.","iconUrl":"http://jira:8080/images/icons/issuetypes/epic.svg","name":"Epic","subtask":false},"components":[],"timespent":null,"timeoriginalestimate":null,"description":null,"project":{"self":"http://jira:8080/rest/api/2/project/10000","id":"10000","key":"TEST","name":"TEST","avatarUrls":{"48x48":"http://jira:8080/secure/projectavatar?avatarId=10324","24x24":"http://jira:8080/secure/projectavatar?size=small&avatarId=10324","16x16":"http://jira:8080/secure/projectavatar?size=xsmall&avatarId=10324","32x32":"http://jira:8080/secure/projectavatar?size=medium&avatarId=10324"}},"fixVersions":[],"aggregatetimespent":null,"resolution":null,"timetracking":{},"customfield_10104":"ghx-label-1","customfield_10105":"0|hzzzzz:","customfield_10106":null,"attachment":[],"aggregatetimeestimate":null,"resolutiondate":null,"workratio":-1,"summary":"Epic1","lastViewed":"2019-04-04T09:31:34.589+0000","watches":{"self":"http://jira:8080/rest/api/2/issue/TEST-1/watchers","watchCount":1,"isWatching":true},"creator":{"self":"http://jira:8080/rest/api/2/user?username=gbaconnier","name":"gbaconnier","key":"gbaconnier","emailAddress":"guewen.baconnier@camptocamp.com","avatarUrls":{"48x48":"https://www.gravatar.com/avatar/ad252192c3f73885676b7d2e850ad13c?d=mm&s=48","24x24":"https://www.gravatar.com/avatar/ad252192c3f73885676b7d2e850ad13c?d=mm&s=24","16x16":"https://www.gravatar.com/avatar/ad252192c3f73885676b7d2e850ad13c?d=mm&s=16","32x32":"https://www.gravatar.com/avatar/ad252192c3f73885676b7d2e850ad13c?d=mm&s=32"},"displayName":"Guewen + Baconnier","active":true,"timeZone":"GMT"},"subtasks":[],"created":"2019-04-04T09:31:27.779+0000","reporter":{"self":"http://jira:8080/rest/api/2/user?username=gbaconnier","name":"gbaconnier","key":"gbaconnier","emailAddress":"guewen.baconnier@camptocamp.com","avatarUrls":{"48x48":"https://www.gravatar.com/avatar/ad252192c3f73885676b7d2e850ad13c?d=mm&s=48","24x24":"https://www.gravatar.com/avatar/ad252192c3f73885676b7d2e850ad13c?d=mm&s=24","16x16":"https://www.gravatar.com/avatar/ad252192c3f73885676b7d2e850ad13c?d=mm&s=16","32x32":"https://www.gravatar.com/avatar/ad252192c3f73885676b7d2e850ad13c?d=mm&s=32"},"displayName":"Guewen + Baconnier","active":true,"timeZone":"GMT"},"customfield_10000":"{summaryBean=com.atlassian.jira.plugin.devstatus.rest.SummaryBean@6f09ec26[summary={pullrequest=com.atlassian.jira.plugin.devstatus.rest.SummaryItemBean@5cacfaad[overall=PullRequestOverallBean{stateCount=0, + state=''OPEN'', details=PullRequestOverallDetails{openCount=0, mergedCount=0, + declinedCount=0}},byInstanceType={}], build=com.atlassian.jira.plugin.devstatus.rest.SummaryItemBean@1ac62104[overall=com.atlassian.jira.plugin.devstatus.summary.beans.BuildOverallBean@2104c963[failedBuildCount=0,successfulBuildCount=0,unknownBuildCount=0,count=0,lastUpdated=,lastUpdatedTimestamp=],byInstanceType={}], + review=com.atlassian.jira.plugin.devstatus.rest.SummaryItemBean@11de62f7[overall=com.atlassian.jira.plugin.devstatus.summary.beans.ReviewsOverallBean@5f0b0109[stateCount=0,state=,dueDate=,overDue=false,count=0,lastUpdated=,lastUpdatedTimestamp=],byInstanceType={}], + deployment-environment=com.atlassian.jira.plugin.devstatus.rest.SummaryItemBean@74eed5ed[overall=com.atlassian.jira.plugin.devstatus.summary.beans.DeploymentOverallBean@732655c2[topEnvironments=[],showProjects=false,successfulCount=0,count=0,lastUpdated=,lastUpdatedTimestamp=],byInstanceType={}], + repository=com.atlassian.jira.plugin.devstatus.rest.SummaryItemBean@110d488b[overall=com.atlassian.jira.plugin.devstatus.summary.beans.CommitOverallBean@168910ea[count=0,lastUpdated=,lastUpdatedTimestamp=],byInstanceType={}], + branch=com.atlassian.jira.plugin.devstatus.rest.SummaryItemBean@144aee6b[overall=com.atlassian.jira.plugin.devstatus.summary.beans.BranchOverallBean@55833165[count=0,lastUpdated=,lastUpdatedTimestamp=],byInstanceType={}]},errors=[],configErrors=[]], + devSummaryJson={\"cachedValue\":{\"errors\":[],\"configErrors\":[],\"summary\":{\"pullrequest\":{\"overall\":{\"count\":0,\"lastUpdated\":null,\"stateCount\":0,\"state\":\"OPEN\",\"details\":{\"openCount\":0,\"mergedCount\":0,\"declinedCount\":0,\"total\":0},\"open\":true},\"byInstanceType\":{}},\"build\":{\"overall\":{\"count\":0,\"lastUpdated\":null,\"failedBuildCount\":0,\"successfulBuildCount\":0,\"unknownBuildCount\":0},\"byInstanceType\":{}},\"review\":{\"overall\":{\"count\":0,\"lastUpdated\":null,\"stateCount\":0,\"state\":null,\"dueDate\":null,\"overDue\":false,\"completed\":false},\"byInstanceType\":{}},\"deployment-environment\":{\"overall\":{\"count\":0,\"lastUpdated\":null,\"topEnvironments\":[],\"showProjects\":false,\"successfulCount\":0},\"byInstanceType\":{}},\"repository\":{\"overall\":{\"count\":0,\"lastUpdated\":null},\"byInstanceType\":{}},\"branch\":{\"overall\":{\"count\":0,\"lastUpdated\":null},\"byInstanceType\":{}}}},\"isStale\":false}}","aggregateprogress":{"progress":0,"total":0},"priority":{"self":"http://jira:8080/rest/api/2/priority/3","iconUrl":"http://jira:8080/images/icons/priorities/medium.svg","name":"Medium","id":"3"},"customfield_10100":null,"customfield_10101":null,"customfield_10102":{"self":"http://jira:8080/rest/api/2/customFieldOption/10000","value":"To + Do","id":"10000"},"labels":[],"customfield_10103":"Epic1","environment":null,"timeestimate":null,"aggregatetimeoriginalestimate":null,"versions":[],"duedate":null,"progress":{"progress":0,"total":0},"comment":{"comments":[],"maxResults":0,"total":0,"startAt":0},"issuelinks":[],"votes":{"self":"http://jira:8080/rest/api/2/issue/TEST-1/votes","votes":0,"hasVoted":false},"worklog":{"startAt":0,"maxResults":20,"total":0,"worklogs":[]},"assignee":null,"updated":"2019-04-04T09:31:27.779+0000","status":{"self":"http://jira:8080/rest/api/2/status/10000","description":"","iconUrl":"http://jira:8080/","name":"To + Do","id":"10000","statusCategory":{"self":"http://jira:8080/rest/api/2/statuscategory/2","id":2,"key":"new","colorName":"blue-gray","name":"To + Do"}}}}'} + headers: + Cache-Control: ['no-cache, no-store, no-transform'] + Content-Security-Policy: [frame-ancestors 'self'] + Content-Type: [application/json;charset=UTF-8] + Date: ['Thu, 04 Apr 2019 09:52:00 GMT'] + Set-Cookie: [JSESSIONID=32B01190C0036F3FFFB97E51C42E4E59; Path=/; HttpOnly] + Transfer-Encoding: [chunked] + Vary: [User-Agent] + X-AREQUESTID: [592x1054x1] + X-ASEN: [SEN-L13384799] + X-ASESSIONID: [jvp381] + X-AUSERNAME: [gbaconnier] + X-Content-Type-Options: [nosniff] + X-Frame-Options: [SAMEORIGIN] + X-Seraph-LoginReason: [OK] + X-XSS-Protection: [1; mode=block] + content-length: ['6416'] + status: {code: 200, message: ''} +version: 1 diff --git a/connector_jira/tests/fixtures/cassettes/test_import_worklog.yaml b/connector_jira/tests/fixtures/cassettes/test_import_worklog.yaml new file mode 100644 index 000000000..27e20927a --- /dev/null +++ b/connector_jira/tests/fixtures/cassettes/test_import_worklog.yaml @@ -0,0 +1,898 @@ +interactions: +- request: + body: null + headers: + Accept: + - !!binary | + YXBwbGljYXRpb24vanNvbiwqLio7cT0wLjk= + Accept-Encoding: + - !!binary | + Z3ppcCwgZGVmbGF0ZQ== + Cache-Control: + - !!binary | + bm8tY2FjaGU= + Connection: + - !!binary | + a2VlcC1hbGl2ZQ== + Content-Type: + - !!binary | + YXBwbGljYXRpb24vanNvbg== + User-Agent: + - !!binary | + cHl0aG9uLXJlcXVlc3RzLzIuMjEuMA== + X-Atlassian-Token: + - !!binary | + bm8tY2hlY2s= + method: GET + uri: http://jira:8080/rest/api/2/serverInfo + response: + body: {string: '{"baseUrl":"http://localhost:8080","version":"7.12.3","versionNumbers":[7,12,3],"deploymentType":"Server","buildNumber":712004,"buildDate":"2018-10-12T00:00:00.000+0000","serverTime":"2019-04-04T11:05:22.439+0000","scmInfo":"5ef91d760d7124da5ebec5c16a948a4a807698df","serverTitle":"Jira"}'} + headers: + Cache-Control: ['no-cache, no-store, no-transform'] + Content-Security-Policy: [frame-ancestors 'self'] + Content-Type: [application/json;charset=UTF-8] + Date: ['Thu, 04 Apr 2019 11:05:22 GMT'] + Set-Cookie: [JSESSIONID=A5826C2FD1625D70986326F8CD1420C7; Path=/; HttpOnly, + atlassian.xsrf.token=BYG3-6SPF-0UM1-2LBO_ea6b5683b480d4a9bf94442ca4379ebee904eb49_lin; + Path=/] + Transfer-Encoding: [chunked] + Vary: [User-Agent] + X-AREQUESTID: [665x1188x1] + X-ASEN: [SEN-L13384799] + X-ASESSIONID: [lrv5n9] + X-AUSERNAME: [gbaconnier] + X-Content-Type-Options: [nosniff] + X-Frame-Options: [SAMEORIGIN] + X-Seraph-LoginReason: [OK] + X-XSS-Protection: [1; mode=block] + content-length: ['288'] + status: {code: 200, message: ''} +- request: + body: null + headers: + Accept: + - !!binary | + YXBwbGljYXRpb24vanNvbiwqLio7cT0wLjk= + Accept-Encoding: + - !!binary | + Z3ppcCwgZGVmbGF0ZQ== + Cache-Control: + - !!binary | + bm8tY2FjaGU= + Connection: + - !!binary | + a2VlcC1hbGl2ZQ== + Content-Type: + - !!binary | + YXBwbGljYXRpb24vanNvbg== + Cookie: + - !!binary | + YXRsYXNzaWFuLnhzcmYudG9rZW49QllHMy02U1BGLTBVTTEtMkxCT19lYTZiNTY4M2I0ODBkNGE5 + YmY5NDQ0MmNhNDM3OWViZWU5MDRlYjQ5X2xpbjsgSlNFU1NJT05JRD1BNTgyNkMyRkQxNjI1RDcw + OTg2MzI2RjhDRDE0MjBDNw== + User-Agent: + - !!binary | + cHl0aG9uLXJlcXVlc3RzLzIuMjEuMA== + X-Atlassian-Token: + - !!binary | + bm8tY2hlY2s= + method: GET + uri: http://jira:8080/rest/api/2/field + response: + body: {string: "[{\"id\":\"issuetype\",\"name\":\"Issue Type\",\"custom\":false,\"\ + orderable\":true,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"\ + issuetype\",\"type\"],\"schema\":{\"type\":\"issuetype\",\"system\":\"issuetype\"\ + }},{\"id\":\"components\",\"name\":\"Component/s\",\"custom\":false,\"orderable\"\ + :true,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"component\"\ + ],\"schema\":{\"type\":\"array\",\"items\":\"component\",\"system\":\"components\"\ + }},{\"id\":\"issuekey\",\"name\":\"Key\",\"custom\":false,\"orderable\":false,\"\ + navigable\":true,\"searchable\":false,\"clauseNames\":[\"id\",\"issue\",\"\ + issuekey\",\"key\"]},{\"id\":\"timespent\",\"name\":\"Time Spent\",\"custom\"\ + :false,\"orderable\":false,\"navigable\":true,\"searchable\":false,\"clauseNames\"\ + :[\"timespent\"],\"schema\":{\"type\":\"number\",\"system\":\"timespent\"\ + }},{\"id\":\"timeoriginalestimate\",\"name\":\"Original Estimate\",\"custom\"\ + :false,\"orderable\":false,\"navigable\":true,\"searchable\":false,\"clauseNames\"\ + :[\"originalEstimate\",\"timeoriginalestimate\"],\"schema\":{\"type\":\"number\"\ + ,\"system\":\"timeoriginalestimate\"}},{\"id\":\"project\",\"name\":\"Project\"\ + ,\"custom\":false,\"orderable\":false,\"navigable\":true,\"searchable\":true,\"\ + clauseNames\":[\"project\"],\"schema\":{\"type\":\"project\",\"system\":\"\ + project\"}},{\"id\":\"description\",\"name\":\"Description\",\"custom\":false,\"\ + orderable\":true,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"\ + description\"],\"schema\":{\"type\":\"string\",\"system\":\"description\"\ + }},{\"id\":\"fixVersions\",\"name\":\"Fix Version/s\",\"custom\":false,\"\ + orderable\":true,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"\ + fixVersion\"],\"schema\":{\"type\":\"array\",\"items\":\"version\",\"system\"\ + :\"fixVersions\"}},{\"id\":\"aggregatetimespent\",\"name\":\"\u03A3 Time Spent\"\ + ,\"custom\":false,\"orderable\":false,\"navigable\":true,\"searchable\":false,\"\ + clauseNames\":[],\"schema\":{\"type\":\"number\",\"system\":\"aggregatetimespent\"\ + }},{\"id\":\"resolution\",\"name\":\"Resolution\",\"custom\":false,\"orderable\"\ + :true,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"resolution\"\ + ],\"schema\":{\"type\":\"resolution\",\"system\":\"resolution\"}},{\"id\"\ + :\"timetracking\",\"name\":\"Time Tracking\",\"custom\":false,\"orderable\"\ + :true,\"navigable\":false,\"searchable\":true,\"clauseNames\":[],\"schema\"\ + :{\"type\":\"timetracking\",\"system\":\"timetracking\"}},{\"id\":\"customfield_10104\"\ + ,\"name\":\"Epic Color\",\"custom\":true,\"orderable\":true,\"navigable\"\ + :true,\"searchable\":true,\"clauseNames\":[\"cf[10104]\",\"Epic Color\"],\"\ + schema\":{\"type\":\"string\",\"custom\":\"com.pyxis.greenhopper.jira:gh-epic-color\"\ + ,\"customId\":10104}},{\"id\":\"customfield_10105\",\"name\":\"Rank\",\"custom\"\ + :true,\"orderable\":true,\"navigable\":true,\"searchable\":true,\"clauseNames\"\ + :[\"cf[10105]\",\"Rank\"],\"schema\":{\"type\":\"any\",\"custom\":\"com.pyxis.greenhopper.jira:gh-lexo-rank\"\ + ,\"customId\":10105}},{\"id\":\"security\",\"name\":\"Security Level\",\"\ + custom\":false,\"orderable\":true,\"navigable\":true,\"searchable\":true,\"\ + clauseNames\":[\"level\"],\"schema\":{\"type\":\"securitylevel\",\"system\"\ + :\"security\"}},{\"id\":\"customfield_10106\",\"name\":\"Story Points\",\"\ + custom\":true,\"orderable\":true,\"navigable\":true,\"searchable\":true,\"\ + clauseNames\":[\"cf[10106]\",\"Story Points\"],\"schema\":{\"type\":\"number\"\ + ,\"custom\":\"com.atlassian.jira.plugin.system.customfieldtypes:float\",\"\ + customId\":10106}},{\"id\":\"attachment\",\"name\":\"Attachment\",\"custom\"\ + :false,\"orderable\":true,\"navigable\":false,\"searchable\":true,\"clauseNames\"\ + :[\"attachments\"],\"schema\":{\"type\":\"array\",\"items\":\"attachment\"\ + ,\"system\":\"attachment\"}},{\"id\":\"aggregatetimeestimate\",\"name\":\"\ + \u03A3 Remaining Estimate\",\"custom\":false,\"orderable\":false,\"navigable\"\ + :true,\"searchable\":false,\"clauseNames\":[],\"schema\":{\"type\":\"number\"\ + ,\"system\":\"aggregatetimeestimate\"}},{\"id\":\"resolutiondate\",\"name\"\ + :\"Resolved\",\"custom\":false,\"orderable\":false,\"navigable\":true,\"searchable\"\ + :true,\"clauseNames\":[\"resolutiondate\",\"resolved\"],\"schema\":{\"type\"\ + :\"datetime\",\"system\":\"resolutiondate\"}},{\"id\":\"workratio\",\"name\"\ + :\"Work Ratio\",\"custom\":false,\"orderable\":false,\"navigable\":true,\"\ + searchable\":true,\"clauseNames\":[\"workratio\"],\"schema\":{\"type\":\"\ + number\",\"system\":\"workratio\"}},{\"id\":\"summary\",\"name\":\"Summary\"\ + ,\"custom\":false,\"orderable\":true,\"navigable\":true,\"searchable\":true,\"\ + clauseNames\":[\"summary\"],\"schema\":{\"type\":\"string\",\"system\":\"\ + summary\"}},{\"id\":\"lastViewed\",\"name\":\"Last Viewed\",\"custom\":false,\"\ + orderable\":false,\"navigable\":true,\"searchable\":false,\"clauseNames\"\ + :[\"lastViewed\"],\"schema\":{\"type\":\"datetime\",\"system\":\"lastViewed\"\ + }},{\"id\":\"watches\",\"name\":\"Watchers\",\"custom\":false,\"orderable\"\ + :false,\"navigable\":true,\"searchable\":false,\"clauseNames\":[\"watchers\"\ + ],\"schema\":{\"type\":\"watches\",\"system\":\"watches\"}},{\"id\":\"creator\"\ + ,\"name\":\"Creator\",\"custom\":false,\"orderable\":false,\"navigable\":true,\"\ + searchable\":true,\"clauseNames\":[\"creator\"],\"schema\":{\"type\":\"user\"\ + ,\"system\":\"creator\"}},{\"id\":\"thumbnail\",\"name\":\"Images\",\"custom\"\ + :false,\"orderable\":false,\"navigable\":true,\"searchable\":false,\"clauseNames\"\ + :[]},{\"id\":\"subtasks\",\"name\":\"Sub-Tasks\",\"custom\":false,\"orderable\"\ + :false,\"navigable\":true,\"searchable\":false,\"clauseNames\":[\"subtasks\"\ + ],\"schema\":{\"type\":\"array\",\"items\":\"issuelinks\",\"system\":\"subtasks\"\ + }},{\"id\":\"created\",\"name\":\"Created\",\"custom\":false,\"orderable\"\ + :false,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"created\"\ + ,\"createdDate\"],\"schema\":{\"type\":\"datetime\",\"system\":\"created\"\ + }},{\"id\":\"reporter\",\"name\":\"Reporter\",\"custom\":false,\"orderable\"\ + :true,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"reporter\"\ + ],\"schema\":{\"type\":\"user\",\"system\":\"reporter\"}},{\"id\":\"aggregateprogress\"\ + ,\"name\":\"\u03A3 Progress\",\"custom\":false,\"orderable\":false,\"navigable\"\ + :true,\"searchable\":false,\"clauseNames\":[],\"schema\":{\"type\":\"progress\"\ + ,\"system\":\"aggregateprogress\"}},{\"id\":\"customfield_10000\",\"name\"\ + :\"Development\",\"custom\":true,\"orderable\":true,\"navigable\":true,\"\ + searchable\":true,\"clauseNames\":[\"cf[10000]\",\"Development\"],\"schema\"\ + :{\"type\":\"any\",\"custom\":\"com.atlassian.jira.plugins.jira-development-integration-plugin:devsummary\"\ + ,\"customId\":10000}},{\"id\":\"priority\",\"name\":\"Priority\",\"custom\"\ + :false,\"orderable\":true,\"navigable\":true,\"searchable\":true,\"clauseNames\"\ + :[\"priority\"],\"schema\":{\"type\":\"priority\",\"system\":\"priority\"\ + }},{\"id\":\"customfield_10100\",\"name\":\"Sprint\",\"custom\":true,\"orderable\"\ + :true,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"cf[10100]\"\ + ,\"Sprint\"],\"schema\":{\"type\":\"array\",\"items\":\"string\",\"custom\"\ + :\"com.pyxis.greenhopper.jira:gh-sprint\",\"customId\":10100}},{\"id\":\"\ + customfield_10101\",\"name\":\"Epic Link\",\"custom\":true,\"orderable\":true,\"\ + navigable\":true,\"searchable\":true,\"clauseNames\":[\"cf[10101]\",\"Epic\ + \ Link\"],\"schema\":{\"type\":\"any\",\"custom\":\"com.pyxis.greenhopper.jira:gh-epic-link\"\ + ,\"customId\":10101}},{\"id\":\"customfield_10102\",\"name\":\"Epic Status\"\ + ,\"custom\":true,\"orderable\":true,\"navigable\":true,\"searchable\":true,\"\ + clauseNames\":[\"cf[10102]\",\"Epic Status\"],\"schema\":{\"type\":\"option\"\ + ,\"custom\":\"com.pyxis.greenhopper.jira:gh-epic-status\",\"customId\":10102}},{\"\ + id\":\"labels\",\"name\":\"Labels\",\"custom\":false,\"orderable\":true,\"\ + navigable\":true,\"searchable\":true,\"clauseNames\":[\"labels\"],\"schema\"\ + :{\"type\":\"array\",\"items\":\"string\",\"system\":\"labels\"}},{\"id\"\ + :\"customfield_10103\",\"name\":\"Epic Name\",\"custom\":true,\"orderable\"\ + :true,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"cf[10103]\"\ + ,\"Epic Name\"],\"schema\":{\"type\":\"string\",\"custom\":\"com.pyxis.greenhopper.jira:gh-epic-label\"\ + ,\"customId\":10103}},{\"id\":\"environment\",\"name\":\"Environment\",\"\ + custom\":false,\"orderable\":true,\"navigable\":true,\"searchable\":true,\"\ + clauseNames\":[\"environment\"],\"schema\":{\"type\":\"string\",\"system\"\ + :\"environment\"}},{\"id\":\"timeestimate\",\"name\":\"Remaining Estimate\"\ + ,\"custom\":false,\"orderable\":false,\"navigable\":true,\"searchable\":false,\"\ + clauseNames\":[\"remainingEstimate\",\"timeestimate\"],\"schema\":{\"type\"\ + :\"number\",\"system\":\"timeestimate\"}},{\"id\":\"aggregatetimeoriginalestimate\"\ + ,\"name\":\"\u03A3 Original Estimate\",\"custom\":false,\"orderable\":false,\"\ + navigable\":true,\"searchable\":false,\"clauseNames\":[],\"schema\":{\"type\"\ + :\"number\",\"system\":\"aggregatetimeoriginalestimate\"}},{\"id\":\"versions\"\ + ,\"name\":\"Affects Version/s\",\"custom\":false,\"orderable\":true,\"navigable\"\ + :true,\"searchable\":true,\"clauseNames\":[\"affectedVersion\"],\"schema\"\ + :{\"type\":\"array\",\"items\":\"version\",\"system\":\"versions\"}},{\"id\"\ + :\"duedate\",\"name\":\"Due Date\",\"custom\":false,\"orderable\":true,\"\ + navigable\":true,\"searchable\":true,\"clauseNames\":[\"due\",\"duedate\"\ + ],\"schema\":{\"type\":\"date\",\"system\":\"duedate\"}},{\"id\":\"progress\"\ + ,\"name\":\"Progress\",\"custom\":false,\"orderable\":false,\"navigable\"\ + :true,\"searchable\":false,\"clauseNames\":[\"progress\"],\"schema\":{\"type\"\ + :\"progress\",\"system\":\"progress\"}},{\"id\":\"comment\",\"name\":\"Comment\"\ + ,\"custom\":false,\"orderable\":true,\"navigable\":false,\"searchable\":true,\"\ + clauseNames\":[\"comment\"],\"schema\":{\"type\":\"comments-page\",\"system\"\ + :\"comment\"}},{\"id\":\"votes\",\"name\":\"Votes\",\"custom\":false,\"orderable\"\ + :false,\"navigable\":true,\"searchable\":false,\"clauseNames\":[\"votes\"\ + ],\"schema\":{\"type\":\"votes\",\"system\":\"votes\"}},{\"id\":\"issuelinks\"\ + ,\"name\":\"Linked Issues\",\"custom\":false,\"orderable\":true,\"navigable\"\ + :true,\"searchable\":true,\"clauseNames\":[],\"schema\":{\"type\":\"array\"\ + ,\"items\":\"issuelinks\",\"system\":\"issuelinks\"}},{\"id\":\"worklog\"\ + ,\"name\":\"Log Work\",\"custom\":false,\"orderable\":true,\"navigable\":false,\"\ + searchable\":true,\"clauseNames\":[],\"schema\":{\"type\":\"array\",\"items\"\ + :\"worklog\",\"system\":\"worklog\"}},{\"id\":\"assignee\",\"name\":\"Assignee\"\ + ,\"custom\":false,\"orderable\":true,\"navigable\":true,\"searchable\":true,\"\ + clauseNames\":[\"assignee\"],\"schema\":{\"type\":\"user\",\"system\":\"assignee\"\ + }},{\"id\":\"updated\",\"name\":\"Updated\",\"custom\":false,\"orderable\"\ + :false,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"updated\"\ + ,\"updatedDate\"],\"schema\":{\"type\":\"datetime\",\"system\":\"updated\"\ + }},{\"id\":\"status\",\"name\":\"Status\",\"custom\":false,\"orderable\":false,\"\ + navigable\":true,\"searchable\":true,\"clauseNames\":[\"status\"],\"schema\"\ + :{\"type\":\"status\",\"system\":\"status\"}}]"} + headers: + Cache-Control: ['no-cache, no-store, no-transform'] + Content-Security-Policy: [frame-ancestors 'self'] + Content-Type: [application/json;charset=UTF-8] + Date: ['Thu, 04 Apr 2019 11:05:22 GMT'] + Set-Cookie: [JSESSIONID=C18D7FF4FFF4377613961E06BCEBBA65; Path=/; HttpOnly] + Transfer-Encoding: [chunked] + Vary: [User-Agent] + X-AREQUESTID: [665x1189x1] + X-ASEN: [SEN-L13384799] + X-ASESSIONID: [fpukwj] + X-AUSERNAME: [gbaconnier] + X-Content-Type-Options: [nosniff] + X-Frame-Options: [SAMEORIGIN] + X-Seraph-LoginReason: [OK] + X-XSS-Protection: [1; mode=block] + content-length: ['9557'] + status: {code: 200, message: ''} +- request: + body: null + headers: + Accept: + - !!binary | + YXBwbGljYXRpb24vanNvbiwqLio7cT0wLjk= + Accept-Encoding: + - !!binary | + Z3ppcCwgZGVmbGF0ZQ== + Cache-Control: + - !!binary | + bm8tY2FjaGU= + Connection: + - !!binary | + a2VlcC1hbGl2ZQ== + Content-Type: + - !!binary | + YXBwbGljYXRpb24vanNvbg== + Cookie: + - !!binary | + YXRsYXNzaWFuLnhzcmYudG9rZW49QllHMy02U1BGLTBVTTEtMkxCT19lYTZiNTY4M2I0ODBkNGE5 + YmY5NDQ0MmNhNDM3OWViZWU5MDRlYjQ5X2xpbjsgSlNFU1NJT05JRD1DMThEN0ZGNEZGRjQzNzc2 + MTM5NjFFMDZCQ0VCQkE2NQ== + User-Agent: + - !!binary | + cHl0aG9uLXJlcXVlc3RzLzIuMjEuMA== + X-Atlassian-Token: + - !!binary | + bm8tY2hlY2s= + method: GET + uri: http://jira:8080/rest/api/2/issue/10000 + response: + body: {string: '{"expand":"renderedFields,names,schema,operations,editmeta,changelog,versionedRepresentations","id":"10000","self":"http://jira:8080/rest/api/2/issue/10000","key":"TEST-1","fields":{"issuetype":{"self":"http://jira:8080/rest/api/2/issuetype/10000","id":"10000","description":"Created + by Jira Software - do not edit or delete. Issue type for a big user story + that needs to be broken down.","iconUrl":"http://jira:8080/images/icons/issuetypes/epic.svg","name":"Epic","subtask":false},"components":[],"timespent":3600,"timeoriginalestimate":null,"description":null,"project":{"self":"http://jira:8080/rest/api/2/project/10000","id":"10000","key":"TEST","name":"TEST","avatarUrls":{"48x48":"http://jira:8080/secure/projectavatar?avatarId=10324","24x24":"http://jira:8080/secure/projectavatar?size=small&avatarId=10324","16x16":"http://jira:8080/secure/projectavatar?size=xsmall&avatarId=10324","32x32":"http://jira:8080/secure/projectavatar?size=medium&avatarId=10324"}},"fixVersions":[],"aggregatetimespent":3600,"resolution":null,"timetracking":{"remainingEstimate":"0m","timeSpent":"1h","remainingEstimateSeconds":0,"timeSpentSeconds":3600},"customfield_10104":"ghx-label-1","customfield_10105":"0|hzzzzz:","customfield_10106":null,"attachment":[],"aggregatetimeestimate":0,"resolutiondate":null,"workratio":-1,"summary":"Epic1","lastViewed":"2019-04-04T11:01:47.707+0000","watches":{"self":"http://jira:8080/rest/api/2/issue/TEST-1/watchers","watchCount":1,"isWatching":true},"creator":{"self":"http://jira:8080/rest/api/2/user?username=gbaconnier","name":"gbaconnier","key":"gbaconnier","emailAddress":"guewen.baconnier@camptocamp.com","avatarUrls":{"48x48":"https://www.gravatar.com/avatar/ad252192c3f73885676b7d2e850ad13c?d=mm&s=48","24x24":"https://www.gravatar.com/avatar/ad252192c3f73885676b7d2e850ad13c?d=mm&s=24","16x16":"https://www.gravatar.com/avatar/ad252192c3f73885676b7d2e850ad13c?d=mm&s=16","32x32":"https://www.gravatar.com/avatar/ad252192c3f73885676b7d2e850ad13c?d=mm&s=32"},"displayName":"Guewen + Baconnier","active":true,"timeZone":"GMT"},"subtasks":[],"created":"2019-04-04T09:31:27.779+0000","reporter":{"self":"http://jira:8080/rest/api/2/user?username=gbaconnier","name":"gbaconnier","key":"gbaconnier","emailAddress":"guewen.baconnier@camptocamp.com","avatarUrls":{"48x48":"https://www.gravatar.com/avatar/ad252192c3f73885676b7d2e850ad13c?d=mm&s=48","24x24":"https://www.gravatar.com/avatar/ad252192c3f73885676b7d2e850ad13c?d=mm&s=24","16x16":"https://www.gravatar.com/avatar/ad252192c3f73885676b7d2e850ad13c?d=mm&s=16","32x32":"https://www.gravatar.com/avatar/ad252192c3f73885676b7d2e850ad13c?d=mm&s=32"},"displayName":"Guewen + Baconnier","active":true,"timeZone":"GMT"},"customfield_10000":"{summaryBean=com.atlassian.jira.plugin.devstatus.rest.SummaryBean@7f99664e[summary={pullrequest=com.atlassian.jira.plugin.devstatus.rest.SummaryItemBean@768f7ebf[overall=PullRequestOverallBean{stateCount=0, + state=''OPEN'', details=PullRequestOverallDetails{openCount=0, mergedCount=0, + declinedCount=0}},byInstanceType={}], build=com.atlassian.jira.plugin.devstatus.rest.SummaryItemBean@3a210d16[overall=com.atlassian.jira.plugin.devstatus.summary.beans.BuildOverallBean@37d6f66b[failedBuildCount=0,successfulBuildCount=0,unknownBuildCount=0,count=0,lastUpdated=,lastUpdatedTimestamp=],byInstanceType={}], + review=com.atlassian.jira.plugin.devstatus.rest.SummaryItemBean@6e422d1d[overall=com.atlassian.jira.plugin.devstatus.summary.beans.ReviewsOverallBean@150bfbb5[stateCount=0,state=,dueDate=,overDue=false,count=0,lastUpdated=,lastUpdatedTimestamp=],byInstanceType={}], + deployment-environment=com.atlassian.jira.plugin.devstatus.rest.SummaryItemBean@27700dbf[overall=com.atlassian.jira.plugin.devstatus.summary.beans.DeploymentOverallBean@2715c294[topEnvironments=[],showProjects=false,successfulCount=0,count=0,lastUpdated=,lastUpdatedTimestamp=],byInstanceType={}], + repository=com.atlassian.jira.plugin.devstatus.rest.SummaryItemBean@584029ae[overall=com.atlassian.jira.plugin.devstatus.summary.beans.CommitOverallBean@12dca740[count=0,lastUpdated=,lastUpdatedTimestamp=],byInstanceType={}], + branch=com.atlassian.jira.plugin.devstatus.rest.SummaryItemBean@35664d51[overall=com.atlassian.jira.plugin.devstatus.summary.beans.BranchOverallBean@3cee380b[count=0,lastUpdated=,lastUpdatedTimestamp=],byInstanceType={}]},errors=[],configErrors=[]], + devSummaryJson={\"cachedValue\":{\"errors\":[],\"configErrors\":[],\"summary\":{\"pullrequest\":{\"overall\":{\"count\":0,\"lastUpdated\":null,\"stateCount\":0,\"state\":\"OPEN\",\"details\":{\"openCount\":0,\"mergedCount\":0,\"declinedCount\":0,\"total\":0},\"open\":true},\"byInstanceType\":{}},\"build\":{\"overall\":{\"count\":0,\"lastUpdated\":null,\"failedBuildCount\":0,\"successfulBuildCount\":0,\"unknownBuildCount\":0},\"byInstanceType\":{}},\"review\":{\"overall\":{\"count\":0,\"lastUpdated\":null,\"stateCount\":0,\"state\":null,\"dueDate\":null,\"overDue\":false,\"completed\":false},\"byInstanceType\":{}},\"deployment-environment\":{\"overall\":{\"count\":0,\"lastUpdated\":null,\"topEnvironments\":[],\"showProjects\":false,\"successfulCount\":0},\"byInstanceType\":{}},\"repository\":{\"overall\":{\"count\":0,\"lastUpdated\":null},\"byInstanceType\":{}},\"branch\":{\"overall\":{\"count\":0,\"lastUpdated\":null},\"byInstanceType\":{}}}},\"isStale\":false}}","aggregateprogress":{"progress":3600,"total":3600,"percent":100},"priority":{"self":"http://jira:8080/rest/api/2/priority/3","iconUrl":"http://jira:8080/images/icons/priorities/medium.svg","name":"Medium","id":"3"},"customfield_10100":null,"customfield_10101":null,"customfield_10102":{"self":"http://jira:8080/rest/api/2/customFieldOption/10000","value":"To + Do","id":"10000"},"labels":[],"customfield_10103":"Epic1","environment":null,"timeestimate":0,"aggregatetimeoriginalestimate":null,"versions":[],"duedate":null,"progress":{"progress":3600,"total":3600,"percent":100},"comment":{"comments":[],"maxResults":0,"total":0,"startAt":0},"issuelinks":[],"votes":{"self":"http://jira:8080/rest/api/2/issue/TEST-1/votes","votes":0,"hasVoted":false},"worklog":{"startAt":0,"maxResults":20,"total":1,"worklogs":[{"self":"http://jira:8080/rest/api/2/issue/10000/worklog/10000","author":{"self":"http://jira:8080/rest/api/2/user?username=gbaconnier","name":"gbaconnier","key":"gbaconnier","emailAddress":"guewen.baconnier@camptocamp.com","avatarUrls":{"48x48":"https://www.gravatar.com/avatar/ad252192c3f73885676b7d2e850ad13c?d=mm&s=48","24x24":"https://www.gravatar.com/avatar/ad252192c3f73885676b7d2e850ad13c?d=mm&s=24","16x16":"https://www.gravatar.com/avatar/ad252192c3f73885676b7d2e850ad13c?d=mm&s=16","32x32":"https://www.gravatar.com/avatar/ad252192c3f73885676b7d2e850ad13c?d=mm&s=32"},"displayName":"Guewen + Baconnier","active":true,"timeZone":"GMT"},"updateAuthor":{"self":"http://jira:8080/rest/api/2/user?username=gbaconnier","name":"gbaconnier","key":"gbaconnier","emailAddress":"guewen.baconnier@camptocamp.com","avatarUrls":{"48x48":"https://www.gravatar.com/avatar/ad252192c3f73885676b7d2e850ad13c?d=mm&s=48","24x24":"https://www.gravatar.com/avatar/ad252192c3f73885676b7d2e850ad13c?d=mm&s=24","16x16":"https://www.gravatar.com/avatar/ad252192c3f73885676b7d2e850ad13c?d=mm&s=16","32x32":"https://www.gravatar.com/avatar/ad252192c3f73885676b7d2e850ad13c?d=mm&s=32"},"displayName":"Guewen + Baconnier","active":true,"timeZone":"GMT"},"comment":"write tests","created":"2019-04-04T11:01:47.597+0000","updated":"2019-04-04T11:01:47.597+0000","started":"2019-04-04T11:01:00.000+0000","timeSpent":"1h","timeSpentSeconds":3600,"id":"10000","issueId":"10000"}]},"assignee":null,"updated":"2019-04-04T11:01:47.600+0000","status":{"self":"http://jira:8080/rest/api/2/status/10000","description":"","iconUrl":"http://jira:8080/","name":"To + Do","id":"10000","statusCategory":{"self":"http://jira:8080/rest/api/2/statuscategory/2","id":2,"key":"new","colorName":"blue-gray","name":"To + Do"}}}}'} + headers: + Cache-Control: ['no-cache, no-store, no-transform'] + Content-Security-Policy: [frame-ancestors 'self'] + Content-Type: [application/json;charset=UTF-8] + Date: ['Thu, 04 Apr 2019 11:05:22 GMT'] + Set-Cookie: [JSESSIONID=C9DEBB551130311DE64BF56CA435BFB6; Path=/; HttpOnly] + Transfer-Encoding: [chunked] + Vary: [User-Agent] + X-AREQUESTID: [665x1190x1] + X-ASEN: [SEN-L13384799] + X-ASESSIONID: [1h1pjl0] + X-AUSERNAME: [gbaconnier] + X-Content-Type-Options: [nosniff] + X-Frame-Options: [SAMEORIGIN] + X-Seraph-LoginReason: [OK] + X-XSS-Protection: [1; mode=block] + content-length: ['7993'] + status: {code: 200, message: ''} +- request: + body: null + headers: + Accept: + - !!binary | + YXBwbGljYXRpb24vanNvbiwqLio7cT0wLjk= + Accept-Encoding: + - !!binary | + Z3ppcCwgZGVmbGF0ZQ== + Cache-Control: + - !!binary | + bm8tY2FjaGU= + Connection: + - !!binary | + a2VlcC1hbGl2ZQ== + Content-Type: + - !!binary | + YXBwbGljYXRpb24vanNvbg== + User-Agent: + - !!binary | + cHl0aG9uLXJlcXVlc3RzLzIuMjEuMA== + X-Atlassian-Token: + - !!binary | + bm8tY2hlY2s= + method: GET + uri: http://jira:8080/rest/api/2/serverInfo + response: + body: {string: '{"baseUrl":"http://localhost:8080","version":"7.12.3","versionNumbers":[7,12,3],"deploymentType":"Server","buildNumber":712004,"buildDate":"2018-10-12T00:00:00.000+0000","serverTime":"2019-04-04T11:05:22.533+0000","scmInfo":"5ef91d760d7124da5ebec5c16a948a4a807698df","serverTitle":"Jira"}'} + headers: + Cache-Control: ['no-cache, no-store, no-transform'] + Content-Security-Policy: [frame-ancestors 'self'] + Content-Type: [application/json;charset=UTF-8] + Date: ['Thu, 04 Apr 2019 11:05:22 GMT'] + Set-Cookie: [JSESSIONID=3EA3E2C907DDF3861DBEE7DDA9DE622A; Path=/; HttpOnly, + atlassian.xsrf.token=BYG3-6SPF-0UM1-2LBO_b59f616f4c6f20b9fe7fa319c55249e02b3e107d_lin; + Path=/] + Transfer-Encoding: [chunked] + Vary: [User-Agent] + X-AREQUESTID: [665x1191x1] + X-ASEN: [SEN-L13384799] + X-ASESSIONID: [avjgeb] + X-AUSERNAME: [gbaconnier] + X-Content-Type-Options: [nosniff] + X-Frame-Options: [SAMEORIGIN] + X-Seraph-LoginReason: [OK] + X-XSS-Protection: [1; mode=block] + content-length: ['288'] + status: {code: 200, message: ''} +- request: + body: null + headers: + Accept: + - !!binary | + YXBwbGljYXRpb24vanNvbiwqLio7cT0wLjk= + Accept-Encoding: + - !!binary | + Z3ppcCwgZGVmbGF0ZQ== + Cache-Control: + - !!binary | + bm8tY2FjaGU= + Connection: + - !!binary | + a2VlcC1hbGl2ZQ== + Content-Type: + - !!binary | + YXBwbGljYXRpb24vanNvbg== + Cookie: + - !!binary | + YXRsYXNzaWFuLnhzcmYudG9rZW49QllHMy02U1BGLTBVTTEtMkxCT19iNTlmNjE2ZjRjNmYyMGI5 + ZmU3ZmEzMTljNTUyNDllMDJiM2UxMDdkX2xpbjsgSlNFU1NJT05JRD0zRUEzRTJDOTA3RERGMzg2 + MURCRUU3RERBOURFNjIyQQ== + User-Agent: + - !!binary | + cHl0aG9uLXJlcXVlc3RzLzIuMjEuMA== + X-Atlassian-Token: + - !!binary | + bm8tY2hlY2s= + method: GET + uri: http://jira:8080/rest/api/2/field + response: + body: {string: "[{\"id\":\"issuetype\",\"name\":\"Issue Type\",\"custom\":false,\"\ + orderable\":true,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"\ + issuetype\",\"type\"],\"schema\":{\"type\":\"issuetype\",\"system\":\"issuetype\"\ + }},{\"id\":\"components\",\"name\":\"Component/s\",\"custom\":false,\"orderable\"\ + :true,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"component\"\ + ],\"schema\":{\"type\":\"array\",\"items\":\"component\",\"system\":\"components\"\ + }},{\"id\":\"issuekey\",\"name\":\"Key\",\"custom\":false,\"orderable\":false,\"\ + navigable\":true,\"searchable\":false,\"clauseNames\":[\"id\",\"issue\",\"\ + issuekey\",\"key\"]},{\"id\":\"timespent\",\"name\":\"Time Spent\",\"custom\"\ + :false,\"orderable\":false,\"navigable\":true,\"searchable\":false,\"clauseNames\"\ + :[\"timespent\"],\"schema\":{\"type\":\"number\",\"system\":\"timespent\"\ + }},{\"id\":\"timeoriginalestimate\",\"name\":\"Original Estimate\",\"custom\"\ + :false,\"orderable\":false,\"navigable\":true,\"searchable\":false,\"clauseNames\"\ + :[\"originalEstimate\",\"timeoriginalestimate\"],\"schema\":{\"type\":\"number\"\ + ,\"system\":\"timeoriginalestimate\"}},{\"id\":\"project\",\"name\":\"Project\"\ + ,\"custom\":false,\"orderable\":false,\"navigable\":true,\"searchable\":true,\"\ + clauseNames\":[\"project\"],\"schema\":{\"type\":\"project\",\"system\":\"\ + project\"}},{\"id\":\"description\",\"name\":\"Description\",\"custom\":false,\"\ + orderable\":true,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"\ + description\"],\"schema\":{\"type\":\"string\",\"system\":\"description\"\ + }},{\"id\":\"fixVersions\",\"name\":\"Fix Version/s\",\"custom\":false,\"\ + orderable\":true,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"\ + fixVersion\"],\"schema\":{\"type\":\"array\",\"items\":\"version\",\"system\"\ + :\"fixVersions\"}},{\"id\":\"aggregatetimespent\",\"name\":\"\u03A3 Time Spent\"\ + ,\"custom\":false,\"orderable\":false,\"navigable\":true,\"searchable\":false,\"\ + clauseNames\":[],\"schema\":{\"type\":\"number\",\"system\":\"aggregatetimespent\"\ + }},{\"id\":\"resolution\",\"name\":\"Resolution\",\"custom\":false,\"orderable\"\ + :true,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"resolution\"\ + ],\"schema\":{\"type\":\"resolution\",\"system\":\"resolution\"}},{\"id\"\ + :\"timetracking\",\"name\":\"Time Tracking\",\"custom\":false,\"orderable\"\ + :true,\"navigable\":false,\"searchable\":true,\"clauseNames\":[],\"schema\"\ + :{\"type\":\"timetracking\",\"system\":\"timetracking\"}},{\"id\":\"customfield_10104\"\ + ,\"name\":\"Epic Color\",\"custom\":true,\"orderable\":true,\"navigable\"\ + :true,\"searchable\":true,\"clauseNames\":[\"cf[10104]\",\"Epic Color\"],\"\ + schema\":{\"type\":\"string\",\"custom\":\"com.pyxis.greenhopper.jira:gh-epic-color\"\ + ,\"customId\":10104}},{\"id\":\"customfield_10105\",\"name\":\"Rank\",\"custom\"\ + :true,\"orderable\":true,\"navigable\":true,\"searchable\":true,\"clauseNames\"\ + :[\"cf[10105]\",\"Rank\"],\"schema\":{\"type\":\"any\",\"custom\":\"com.pyxis.greenhopper.jira:gh-lexo-rank\"\ + ,\"customId\":10105}},{\"id\":\"security\",\"name\":\"Security Level\",\"\ + custom\":false,\"orderable\":true,\"navigable\":true,\"searchable\":true,\"\ + clauseNames\":[\"level\"],\"schema\":{\"type\":\"securitylevel\",\"system\"\ + :\"security\"}},{\"id\":\"customfield_10106\",\"name\":\"Story Points\",\"\ + custom\":true,\"orderable\":true,\"navigable\":true,\"searchable\":true,\"\ + clauseNames\":[\"cf[10106]\",\"Story Points\"],\"schema\":{\"type\":\"number\"\ + ,\"custom\":\"com.atlassian.jira.plugin.system.customfieldtypes:float\",\"\ + customId\":10106}},{\"id\":\"attachment\",\"name\":\"Attachment\",\"custom\"\ + :false,\"orderable\":true,\"navigable\":false,\"searchable\":true,\"clauseNames\"\ + :[\"attachments\"],\"schema\":{\"type\":\"array\",\"items\":\"attachment\"\ + ,\"system\":\"attachment\"}},{\"id\":\"aggregatetimeestimate\",\"name\":\"\ + \u03A3 Remaining Estimate\",\"custom\":false,\"orderable\":false,\"navigable\"\ + :true,\"searchable\":false,\"clauseNames\":[],\"schema\":{\"type\":\"number\"\ + ,\"system\":\"aggregatetimeestimate\"}},{\"id\":\"resolutiondate\",\"name\"\ + :\"Resolved\",\"custom\":false,\"orderable\":false,\"navigable\":true,\"searchable\"\ + :true,\"clauseNames\":[\"resolutiondate\",\"resolved\"],\"schema\":{\"type\"\ + :\"datetime\",\"system\":\"resolutiondate\"}},{\"id\":\"workratio\",\"name\"\ + :\"Work Ratio\",\"custom\":false,\"orderable\":false,\"navigable\":true,\"\ + searchable\":true,\"clauseNames\":[\"workratio\"],\"schema\":{\"type\":\"\ + number\",\"system\":\"workratio\"}},{\"id\":\"summary\",\"name\":\"Summary\"\ + ,\"custom\":false,\"orderable\":true,\"navigable\":true,\"searchable\":true,\"\ + clauseNames\":[\"summary\"],\"schema\":{\"type\":\"string\",\"system\":\"\ + summary\"}},{\"id\":\"lastViewed\",\"name\":\"Last Viewed\",\"custom\":false,\"\ + orderable\":false,\"navigable\":true,\"searchable\":false,\"clauseNames\"\ + :[\"lastViewed\"],\"schema\":{\"type\":\"datetime\",\"system\":\"lastViewed\"\ + }},{\"id\":\"watches\",\"name\":\"Watchers\",\"custom\":false,\"orderable\"\ + :false,\"navigable\":true,\"searchable\":false,\"clauseNames\":[\"watchers\"\ + ],\"schema\":{\"type\":\"watches\",\"system\":\"watches\"}},{\"id\":\"creator\"\ + ,\"name\":\"Creator\",\"custom\":false,\"orderable\":false,\"navigable\":true,\"\ + searchable\":true,\"clauseNames\":[\"creator\"],\"schema\":{\"type\":\"user\"\ + ,\"system\":\"creator\"}},{\"id\":\"thumbnail\",\"name\":\"Images\",\"custom\"\ + :false,\"orderable\":false,\"navigable\":true,\"searchable\":false,\"clauseNames\"\ + :[]},{\"id\":\"subtasks\",\"name\":\"Sub-Tasks\",\"custom\":false,\"orderable\"\ + :false,\"navigable\":true,\"searchable\":false,\"clauseNames\":[\"subtasks\"\ + ],\"schema\":{\"type\":\"array\",\"items\":\"issuelinks\",\"system\":\"subtasks\"\ + }},{\"id\":\"created\",\"name\":\"Created\",\"custom\":false,\"orderable\"\ + :false,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"created\"\ + ,\"createdDate\"],\"schema\":{\"type\":\"datetime\",\"system\":\"created\"\ + }},{\"id\":\"reporter\",\"name\":\"Reporter\",\"custom\":false,\"orderable\"\ + :true,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"reporter\"\ + ],\"schema\":{\"type\":\"user\",\"system\":\"reporter\"}},{\"id\":\"aggregateprogress\"\ + ,\"name\":\"\u03A3 Progress\",\"custom\":false,\"orderable\":false,\"navigable\"\ + :true,\"searchable\":false,\"clauseNames\":[],\"schema\":{\"type\":\"progress\"\ + ,\"system\":\"aggregateprogress\"}},{\"id\":\"customfield_10000\",\"name\"\ + :\"Development\",\"custom\":true,\"orderable\":true,\"navigable\":true,\"\ + searchable\":true,\"clauseNames\":[\"cf[10000]\",\"Development\"],\"schema\"\ + :{\"type\":\"any\",\"custom\":\"com.atlassian.jira.plugins.jira-development-integration-plugin:devsummary\"\ + ,\"customId\":10000}},{\"id\":\"priority\",\"name\":\"Priority\",\"custom\"\ + :false,\"orderable\":true,\"navigable\":true,\"searchable\":true,\"clauseNames\"\ + :[\"priority\"],\"schema\":{\"type\":\"priority\",\"system\":\"priority\"\ + }},{\"id\":\"customfield_10100\",\"name\":\"Sprint\",\"custom\":true,\"orderable\"\ + :true,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"cf[10100]\"\ + ,\"Sprint\"],\"schema\":{\"type\":\"array\",\"items\":\"string\",\"custom\"\ + :\"com.pyxis.greenhopper.jira:gh-sprint\",\"customId\":10100}},{\"id\":\"\ + customfield_10101\",\"name\":\"Epic Link\",\"custom\":true,\"orderable\":true,\"\ + navigable\":true,\"searchable\":true,\"clauseNames\":[\"cf[10101]\",\"Epic\ + \ Link\"],\"schema\":{\"type\":\"any\",\"custom\":\"com.pyxis.greenhopper.jira:gh-epic-link\"\ + ,\"customId\":10101}},{\"id\":\"customfield_10102\",\"name\":\"Epic Status\"\ + ,\"custom\":true,\"orderable\":true,\"navigable\":true,\"searchable\":true,\"\ + clauseNames\":[\"cf[10102]\",\"Epic Status\"],\"schema\":{\"type\":\"option\"\ + ,\"custom\":\"com.pyxis.greenhopper.jira:gh-epic-status\",\"customId\":10102}},{\"\ + id\":\"labels\",\"name\":\"Labels\",\"custom\":false,\"orderable\":true,\"\ + navigable\":true,\"searchable\":true,\"clauseNames\":[\"labels\"],\"schema\"\ + :{\"type\":\"array\",\"items\":\"string\",\"system\":\"labels\"}},{\"id\"\ + :\"customfield_10103\",\"name\":\"Epic Name\",\"custom\":true,\"orderable\"\ + :true,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"cf[10103]\"\ + ,\"Epic Name\"],\"schema\":{\"type\":\"string\",\"custom\":\"com.pyxis.greenhopper.jira:gh-epic-label\"\ + ,\"customId\":10103}},{\"id\":\"environment\",\"name\":\"Environment\",\"\ + custom\":false,\"orderable\":true,\"navigable\":true,\"searchable\":true,\"\ + clauseNames\":[\"environment\"],\"schema\":{\"type\":\"string\",\"system\"\ + :\"environment\"}},{\"id\":\"timeestimate\",\"name\":\"Remaining Estimate\"\ + ,\"custom\":false,\"orderable\":false,\"navigable\":true,\"searchable\":false,\"\ + clauseNames\":[\"remainingEstimate\",\"timeestimate\"],\"schema\":{\"type\"\ + :\"number\",\"system\":\"timeestimate\"}},{\"id\":\"aggregatetimeoriginalestimate\"\ + ,\"name\":\"\u03A3 Original Estimate\",\"custom\":false,\"orderable\":false,\"\ + navigable\":true,\"searchable\":false,\"clauseNames\":[],\"schema\":{\"type\"\ + :\"number\",\"system\":\"aggregatetimeoriginalestimate\"}},{\"id\":\"versions\"\ + ,\"name\":\"Affects Version/s\",\"custom\":false,\"orderable\":true,\"navigable\"\ + :true,\"searchable\":true,\"clauseNames\":[\"affectedVersion\"],\"schema\"\ + :{\"type\":\"array\",\"items\":\"version\",\"system\":\"versions\"}},{\"id\"\ + :\"duedate\",\"name\":\"Due Date\",\"custom\":false,\"orderable\":true,\"\ + navigable\":true,\"searchable\":true,\"clauseNames\":[\"due\",\"duedate\"\ + ],\"schema\":{\"type\":\"date\",\"system\":\"duedate\"}},{\"id\":\"progress\"\ + ,\"name\":\"Progress\",\"custom\":false,\"orderable\":false,\"navigable\"\ + :true,\"searchable\":false,\"clauseNames\":[\"progress\"],\"schema\":{\"type\"\ + :\"progress\",\"system\":\"progress\"}},{\"id\":\"comment\",\"name\":\"Comment\"\ + ,\"custom\":false,\"orderable\":true,\"navigable\":false,\"searchable\":true,\"\ + clauseNames\":[\"comment\"],\"schema\":{\"type\":\"comments-page\",\"system\"\ + :\"comment\"}},{\"id\":\"votes\",\"name\":\"Votes\",\"custom\":false,\"orderable\"\ + :false,\"navigable\":true,\"searchable\":false,\"clauseNames\":[\"votes\"\ + ],\"schema\":{\"type\":\"votes\",\"system\":\"votes\"}},{\"id\":\"issuelinks\"\ + ,\"name\":\"Linked Issues\",\"custom\":false,\"orderable\":true,\"navigable\"\ + :true,\"searchable\":true,\"clauseNames\":[],\"schema\":{\"type\":\"array\"\ + ,\"items\":\"issuelinks\",\"system\":\"issuelinks\"}},{\"id\":\"worklog\"\ + ,\"name\":\"Log Work\",\"custom\":false,\"orderable\":true,\"navigable\":false,\"\ + searchable\":true,\"clauseNames\":[],\"schema\":{\"type\":\"array\",\"items\"\ + :\"worklog\",\"system\":\"worklog\"}},{\"id\":\"assignee\",\"name\":\"Assignee\"\ + ,\"custom\":false,\"orderable\":true,\"navigable\":true,\"searchable\":true,\"\ + clauseNames\":[\"assignee\"],\"schema\":{\"type\":\"user\",\"system\":\"assignee\"\ + }},{\"id\":\"updated\",\"name\":\"Updated\",\"custom\":false,\"orderable\"\ + :false,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"updated\"\ + ,\"updatedDate\"],\"schema\":{\"type\":\"datetime\",\"system\":\"updated\"\ + }},{\"id\":\"status\",\"name\":\"Status\",\"custom\":false,\"orderable\":false,\"\ + navigable\":true,\"searchable\":true,\"clauseNames\":[\"status\"],\"schema\"\ + :{\"type\":\"status\",\"system\":\"status\"}}]"} + headers: + Cache-Control: ['no-cache, no-store, no-transform'] + Content-Security-Policy: [frame-ancestors 'self'] + Content-Type: [application/json;charset=UTF-8] + Date: ['Thu, 04 Apr 2019 11:05:22 GMT'] + Set-Cookie: [JSESSIONID=446E3CD6530FE626F77C95E850654B91; Path=/; HttpOnly] + Transfer-Encoding: [chunked] + Vary: [User-Agent] + X-AREQUESTID: [665x1192x1] + X-ASEN: [SEN-L13384799] + X-ASESSIONID: [11w5e3e] + X-AUSERNAME: [gbaconnier] + X-Content-Type-Options: [nosniff] + X-Frame-Options: [SAMEORIGIN] + X-Seraph-LoginReason: [OK] + X-XSS-Protection: [1; mode=block] + content-length: ['9557'] + status: {code: 200, message: ''} +- request: + body: null + headers: + Accept: + - !!binary | + YXBwbGljYXRpb24vanNvbiwqLio7cT0wLjk= + Accept-Encoding: + - !!binary | + Z3ppcCwgZGVmbGF0ZQ== + Cache-Control: + - !!binary | + bm8tY2FjaGU= + Connection: + - !!binary | + a2VlcC1hbGl2ZQ== + Content-Type: + - !!binary | + YXBwbGljYXRpb24vanNvbg== + Cookie: + - !!binary | + YXRsYXNzaWFuLnhzcmYudG9rZW49QllHMy02U1BGLTBVTTEtMkxCT19iNTlmNjE2ZjRjNmYyMGI5 + ZmU3ZmEzMTljNTUyNDllMDJiM2UxMDdkX2xpbjsgSlNFU1NJT05JRD00NDZFM0NENjUzMEZFNjI2 + Rjc3Qzk1RTg1MDY1NEI5MQ== + User-Agent: + - !!binary | + cHl0aG9uLXJlcXVlc3RzLzIuMjEuMA== + X-Atlassian-Token: + - !!binary | + bm8tY2hlY2s= + method: GET + uri: http://jira:8080/rest/api/2/issue/10000/worklog/10000 + response: + body: {string: '{"self":"http://jira:8080/rest/api/2/issue/10000/worklog/10000","author":{"self":"http://jira:8080/rest/api/2/user?username=gbaconnier","name":"gbaconnier","key":"gbaconnier","emailAddress":"guewen.baconnier@camptocamp.com","avatarUrls":{"48x48":"https://www.gravatar.com/avatar/ad252192c3f73885676b7d2e850ad13c?d=mm&s=48","24x24":"https://www.gravatar.com/avatar/ad252192c3f73885676b7d2e850ad13c?d=mm&s=24","16x16":"https://www.gravatar.com/avatar/ad252192c3f73885676b7d2e850ad13c?d=mm&s=16","32x32":"https://www.gravatar.com/avatar/ad252192c3f73885676b7d2e850ad13c?d=mm&s=32"},"displayName":"Guewen + Baconnier","active":true,"timeZone":"GMT"},"updateAuthor":{"self":"http://jira:8080/rest/api/2/user?username=gbaconnier","name":"gbaconnier","key":"gbaconnier","emailAddress":"guewen.baconnier@camptocamp.com","avatarUrls":{"48x48":"https://www.gravatar.com/avatar/ad252192c3f73885676b7d2e850ad13c?d=mm&s=48","24x24":"https://www.gravatar.com/avatar/ad252192c3f73885676b7d2e850ad13c?d=mm&s=24","16x16":"https://www.gravatar.com/avatar/ad252192c3f73885676b7d2e850ad13c?d=mm&s=16","32x32":"https://www.gravatar.com/avatar/ad252192c3f73885676b7d2e850ad13c?d=mm&s=32"},"displayName":"Guewen + Baconnier","active":true,"timeZone":"GMT"},"comment":"write tests","created":"2019-04-04T11:01:47.597+0000","updated":"2019-04-04T11:01:47.597+0000","started":"2019-04-04T11:01:00.000+0000","timeSpent":"1h","timeSpentSeconds":3600,"id":"10000","issueId":"10000"}'} + headers: + Cache-Control: ['no-cache, no-store, no-transform'] + Content-Security-Policy: [frame-ancestors 'self'] + Content-Type: [application/json;charset=UTF-8] + Date: ['Thu, 04 Apr 2019 11:05:22 GMT'] + Set-Cookie: [JSESSIONID=D44292D434F41C49779F0565EEFAEFDB; Path=/; HttpOnly] + Transfer-Encoding: [chunked] + Vary: [User-Agent] + X-AREQUESTID: [665x1193x1] + X-ASEN: [SEN-L13384799] + X-ASESSIONID: [6j0wwu] + X-AUSERNAME: [gbaconnier] + X-Content-Type-Options: [nosniff] + X-Frame-Options: [SAMEORIGIN] + X-Seraph-LoginReason: [OK] + X-XSS-Protection: [1; mode=block] + content-length: ['1449'] + status: {code: 200, message: ''} +- request: + body: null + headers: + Accept: + - !!binary | + YXBwbGljYXRpb24vanNvbiwqLio7cT0wLjk= + Accept-Encoding: + - !!binary | + Z3ppcCwgZGVmbGF0ZQ== + Cache-Control: + - !!binary | + bm8tY2FjaGU= + Connection: + - !!binary | + a2VlcC1hbGl2ZQ== + Content-Type: + - !!binary | + YXBwbGljYXRpb24vanNvbg== + User-Agent: + - !!binary | + cHl0aG9uLXJlcXVlc3RzLzIuMjEuMA== + X-Atlassian-Token: + - !!binary | + bm8tY2hlY2s= + method: GET + uri: http://jira:8080/rest/api/2/serverInfo + response: + body: {string: '{"baseUrl":"http://localhost:8080","version":"7.12.3","versionNumbers":[7,12,3],"deploymentType":"Server","buildNumber":712004,"buildDate":"2018-10-12T00:00:00.000+0000","serverTime":"2019-04-04T11:05:22.619+0000","scmInfo":"5ef91d760d7124da5ebec5c16a948a4a807698df","serverTitle":"Jira"}'} + headers: + Cache-Control: ['no-cache, no-store, no-transform'] + Content-Security-Policy: [frame-ancestors 'self'] + Content-Type: [application/json;charset=UTF-8] + Date: ['Thu, 04 Apr 2019 11:05:22 GMT'] + Set-Cookie: [JSESSIONID=8A055B3EA9D15BA4A44736869555FDE8; Path=/; HttpOnly, + atlassian.xsrf.token=BYG3-6SPF-0UM1-2LBO_70f2fff66d88c7b6ee78296a47871c363d17dc0c_lin; + Path=/] + Transfer-Encoding: [chunked] + Vary: [User-Agent] + X-AREQUESTID: [665x1194x1] + X-ASEN: [SEN-L13384799] + X-ASESSIONID: [pe0lw9] + X-AUSERNAME: [gbaconnier] + X-Content-Type-Options: [nosniff] + X-Frame-Options: [SAMEORIGIN] + X-Seraph-LoginReason: [OK] + X-XSS-Protection: [1; mode=block] + content-length: ['288'] + status: {code: 200, message: ''} +- request: + body: null + headers: + Accept: + - !!binary | + YXBwbGljYXRpb24vanNvbiwqLio7cT0wLjk= + Accept-Encoding: + - !!binary | + Z3ppcCwgZGVmbGF0ZQ== + Cache-Control: + - !!binary | + bm8tY2FjaGU= + Connection: + - !!binary | + a2VlcC1hbGl2ZQ== + Content-Type: + - !!binary | + YXBwbGljYXRpb24vanNvbg== + Cookie: + - !!binary | + YXRsYXNzaWFuLnhzcmYudG9rZW49QllHMy02U1BGLTBVTTEtMkxCT183MGYyZmZmNjZkODhjN2I2 + ZWU3ODI5NmE0Nzg3MWMzNjNkMTdkYzBjX2xpbjsgSlNFU1NJT05JRD04QTA1NUIzRUE5RDE1QkE0 + QTQ0NzM2ODY5NTU1RkRFOA== + User-Agent: + - !!binary | + cHl0aG9uLXJlcXVlc3RzLzIuMjEuMA== + X-Atlassian-Token: + - !!binary | + bm8tY2hlY2s= + method: GET + uri: http://jira:8080/rest/api/2/field + response: + body: {string: "[{\"id\":\"issuetype\",\"name\":\"Issue Type\",\"custom\":false,\"\ + orderable\":true,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"\ + issuetype\",\"type\"],\"schema\":{\"type\":\"issuetype\",\"system\":\"issuetype\"\ + }},{\"id\":\"components\",\"name\":\"Component/s\",\"custom\":false,\"orderable\"\ + :true,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"component\"\ + ],\"schema\":{\"type\":\"array\",\"items\":\"component\",\"system\":\"components\"\ + }},{\"id\":\"issuekey\",\"name\":\"Key\",\"custom\":false,\"orderable\":false,\"\ + navigable\":true,\"searchable\":false,\"clauseNames\":[\"id\",\"issue\",\"\ + issuekey\",\"key\"]},{\"id\":\"timespent\",\"name\":\"Time Spent\",\"custom\"\ + :false,\"orderable\":false,\"navigable\":true,\"searchable\":false,\"clauseNames\"\ + :[\"timespent\"],\"schema\":{\"type\":\"number\",\"system\":\"timespent\"\ + }},{\"id\":\"timeoriginalestimate\",\"name\":\"Original Estimate\",\"custom\"\ + :false,\"orderable\":false,\"navigable\":true,\"searchable\":false,\"clauseNames\"\ + :[\"originalEstimate\",\"timeoriginalestimate\"],\"schema\":{\"type\":\"number\"\ + ,\"system\":\"timeoriginalestimate\"}},{\"id\":\"project\",\"name\":\"Project\"\ + ,\"custom\":false,\"orderable\":false,\"navigable\":true,\"searchable\":true,\"\ + clauseNames\":[\"project\"],\"schema\":{\"type\":\"project\",\"system\":\"\ + project\"}},{\"id\":\"description\",\"name\":\"Description\",\"custom\":false,\"\ + orderable\":true,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"\ + description\"],\"schema\":{\"type\":\"string\",\"system\":\"description\"\ + }},{\"id\":\"fixVersions\",\"name\":\"Fix Version/s\",\"custom\":false,\"\ + orderable\":true,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"\ + fixVersion\"],\"schema\":{\"type\":\"array\",\"items\":\"version\",\"system\"\ + :\"fixVersions\"}},{\"id\":\"aggregatetimespent\",\"name\":\"\u03A3 Time Spent\"\ + ,\"custom\":false,\"orderable\":false,\"navigable\":true,\"searchable\":false,\"\ + clauseNames\":[],\"schema\":{\"type\":\"number\",\"system\":\"aggregatetimespent\"\ + }},{\"id\":\"resolution\",\"name\":\"Resolution\",\"custom\":false,\"orderable\"\ + :true,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"resolution\"\ + ],\"schema\":{\"type\":\"resolution\",\"system\":\"resolution\"}},{\"id\"\ + :\"timetracking\",\"name\":\"Time Tracking\",\"custom\":false,\"orderable\"\ + :true,\"navigable\":false,\"searchable\":true,\"clauseNames\":[],\"schema\"\ + :{\"type\":\"timetracking\",\"system\":\"timetracking\"}},{\"id\":\"customfield_10104\"\ + ,\"name\":\"Epic Color\",\"custom\":true,\"orderable\":true,\"navigable\"\ + :true,\"searchable\":true,\"clauseNames\":[\"cf[10104]\",\"Epic Color\"],\"\ + schema\":{\"type\":\"string\",\"custom\":\"com.pyxis.greenhopper.jira:gh-epic-color\"\ + ,\"customId\":10104}},{\"id\":\"customfield_10105\",\"name\":\"Rank\",\"custom\"\ + :true,\"orderable\":true,\"navigable\":true,\"searchable\":true,\"clauseNames\"\ + :[\"cf[10105]\",\"Rank\"],\"schema\":{\"type\":\"any\",\"custom\":\"com.pyxis.greenhopper.jira:gh-lexo-rank\"\ + ,\"customId\":10105}},{\"id\":\"security\",\"name\":\"Security Level\",\"\ + custom\":false,\"orderable\":true,\"navigable\":true,\"searchable\":true,\"\ + clauseNames\":[\"level\"],\"schema\":{\"type\":\"securitylevel\",\"system\"\ + :\"security\"}},{\"id\":\"customfield_10106\",\"name\":\"Story Points\",\"\ + custom\":true,\"orderable\":true,\"navigable\":true,\"searchable\":true,\"\ + clauseNames\":[\"cf[10106]\",\"Story Points\"],\"schema\":{\"type\":\"number\"\ + ,\"custom\":\"com.atlassian.jira.plugin.system.customfieldtypes:float\",\"\ + customId\":10106}},{\"id\":\"attachment\",\"name\":\"Attachment\",\"custom\"\ + :false,\"orderable\":true,\"navigable\":false,\"searchable\":true,\"clauseNames\"\ + :[\"attachments\"],\"schema\":{\"type\":\"array\",\"items\":\"attachment\"\ + ,\"system\":\"attachment\"}},{\"id\":\"aggregatetimeestimate\",\"name\":\"\ + \u03A3 Remaining Estimate\",\"custom\":false,\"orderable\":false,\"navigable\"\ + :true,\"searchable\":false,\"clauseNames\":[],\"schema\":{\"type\":\"number\"\ + ,\"system\":\"aggregatetimeestimate\"}},{\"id\":\"resolutiondate\",\"name\"\ + :\"Resolved\",\"custom\":false,\"orderable\":false,\"navigable\":true,\"searchable\"\ + :true,\"clauseNames\":[\"resolutiondate\",\"resolved\"],\"schema\":{\"type\"\ + :\"datetime\",\"system\":\"resolutiondate\"}},{\"id\":\"workratio\",\"name\"\ + :\"Work Ratio\",\"custom\":false,\"orderable\":false,\"navigable\":true,\"\ + searchable\":true,\"clauseNames\":[\"workratio\"],\"schema\":{\"type\":\"\ + number\",\"system\":\"workratio\"}},{\"id\":\"summary\",\"name\":\"Summary\"\ + ,\"custom\":false,\"orderable\":true,\"navigable\":true,\"searchable\":true,\"\ + clauseNames\":[\"summary\"],\"schema\":{\"type\":\"string\",\"system\":\"\ + summary\"}},{\"id\":\"lastViewed\",\"name\":\"Last Viewed\",\"custom\":false,\"\ + orderable\":false,\"navigable\":true,\"searchable\":false,\"clauseNames\"\ + :[\"lastViewed\"],\"schema\":{\"type\":\"datetime\",\"system\":\"lastViewed\"\ + }},{\"id\":\"watches\",\"name\":\"Watchers\",\"custom\":false,\"orderable\"\ + :false,\"navigable\":true,\"searchable\":false,\"clauseNames\":[\"watchers\"\ + ],\"schema\":{\"type\":\"watches\",\"system\":\"watches\"}},{\"id\":\"creator\"\ + ,\"name\":\"Creator\",\"custom\":false,\"orderable\":false,\"navigable\":true,\"\ + searchable\":true,\"clauseNames\":[\"creator\"],\"schema\":{\"type\":\"user\"\ + ,\"system\":\"creator\"}},{\"id\":\"thumbnail\",\"name\":\"Images\",\"custom\"\ + :false,\"orderable\":false,\"navigable\":true,\"searchable\":false,\"clauseNames\"\ + :[]},{\"id\":\"subtasks\",\"name\":\"Sub-Tasks\",\"custom\":false,\"orderable\"\ + :false,\"navigable\":true,\"searchable\":false,\"clauseNames\":[\"subtasks\"\ + ],\"schema\":{\"type\":\"array\",\"items\":\"issuelinks\",\"system\":\"subtasks\"\ + }},{\"id\":\"created\",\"name\":\"Created\",\"custom\":false,\"orderable\"\ + :false,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"created\"\ + ,\"createdDate\"],\"schema\":{\"type\":\"datetime\",\"system\":\"created\"\ + }},{\"id\":\"reporter\",\"name\":\"Reporter\",\"custom\":false,\"orderable\"\ + :true,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"reporter\"\ + ],\"schema\":{\"type\":\"user\",\"system\":\"reporter\"}},{\"id\":\"aggregateprogress\"\ + ,\"name\":\"\u03A3 Progress\",\"custom\":false,\"orderable\":false,\"navigable\"\ + :true,\"searchable\":false,\"clauseNames\":[],\"schema\":{\"type\":\"progress\"\ + ,\"system\":\"aggregateprogress\"}},{\"id\":\"customfield_10000\",\"name\"\ + :\"Development\",\"custom\":true,\"orderable\":true,\"navigable\":true,\"\ + searchable\":true,\"clauseNames\":[\"cf[10000]\",\"Development\"],\"schema\"\ + :{\"type\":\"any\",\"custom\":\"com.atlassian.jira.plugins.jira-development-integration-plugin:devsummary\"\ + ,\"customId\":10000}},{\"id\":\"priority\",\"name\":\"Priority\",\"custom\"\ + :false,\"orderable\":true,\"navigable\":true,\"searchable\":true,\"clauseNames\"\ + :[\"priority\"],\"schema\":{\"type\":\"priority\",\"system\":\"priority\"\ + }},{\"id\":\"customfield_10100\",\"name\":\"Sprint\",\"custom\":true,\"orderable\"\ + :true,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"cf[10100]\"\ + ,\"Sprint\"],\"schema\":{\"type\":\"array\",\"items\":\"string\",\"custom\"\ + :\"com.pyxis.greenhopper.jira:gh-sprint\",\"customId\":10100}},{\"id\":\"\ + customfield_10101\",\"name\":\"Epic Link\",\"custom\":true,\"orderable\":true,\"\ + navigable\":true,\"searchable\":true,\"clauseNames\":[\"cf[10101]\",\"Epic\ + \ Link\"],\"schema\":{\"type\":\"any\",\"custom\":\"com.pyxis.greenhopper.jira:gh-epic-link\"\ + ,\"customId\":10101}},{\"id\":\"customfield_10102\",\"name\":\"Epic Status\"\ + ,\"custom\":true,\"orderable\":true,\"navigable\":true,\"searchable\":true,\"\ + clauseNames\":[\"cf[10102]\",\"Epic Status\"],\"schema\":{\"type\":\"option\"\ + ,\"custom\":\"com.pyxis.greenhopper.jira:gh-epic-status\",\"customId\":10102}},{\"\ + id\":\"labels\",\"name\":\"Labels\",\"custom\":false,\"orderable\":true,\"\ + navigable\":true,\"searchable\":true,\"clauseNames\":[\"labels\"],\"schema\"\ + :{\"type\":\"array\",\"items\":\"string\",\"system\":\"labels\"}},{\"id\"\ + :\"customfield_10103\",\"name\":\"Epic Name\",\"custom\":true,\"orderable\"\ + :true,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"cf[10103]\"\ + ,\"Epic Name\"],\"schema\":{\"type\":\"string\",\"custom\":\"com.pyxis.greenhopper.jira:gh-epic-label\"\ + ,\"customId\":10103}},{\"id\":\"environment\",\"name\":\"Environment\",\"\ + custom\":false,\"orderable\":true,\"navigable\":true,\"searchable\":true,\"\ + clauseNames\":[\"environment\"],\"schema\":{\"type\":\"string\",\"system\"\ + :\"environment\"}},{\"id\":\"timeestimate\",\"name\":\"Remaining Estimate\"\ + ,\"custom\":false,\"orderable\":false,\"navigable\":true,\"searchable\":false,\"\ + clauseNames\":[\"remainingEstimate\",\"timeestimate\"],\"schema\":{\"type\"\ + :\"number\",\"system\":\"timeestimate\"}},{\"id\":\"aggregatetimeoriginalestimate\"\ + ,\"name\":\"\u03A3 Original Estimate\",\"custom\":false,\"orderable\":false,\"\ + navigable\":true,\"searchable\":false,\"clauseNames\":[],\"schema\":{\"type\"\ + :\"number\",\"system\":\"aggregatetimeoriginalestimate\"}},{\"id\":\"versions\"\ + ,\"name\":\"Affects Version/s\",\"custom\":false,\"orderable\":true,\"navigable\"\ + :true,\"searchable\":true,\"clauseNames\":[\"affectedVersion\"],\"schema\"\ + :{\"type\":\"array\",\"items\":\"version\",\"system\":\"versions\"}},{\"id\"\ + :\"duedate\",\"name\":\"Due Date\",\"custom\":false,\"orderable\":true,\"\ + navigable\":true,\"searchable\":true,\"clauseNames\":[\"due\",\"duedate\"\ + ],\"schema\":{\"type\":\"date\",\"system\":\"duedate\"}},{\"id\":\"progress\"\ + ,\"name\":\"Progress\",\"custom\":false,\"orderable\":false,\"navigable\"\ + :true,\"searchable\":false,\"clauseNames\":[\"progress\"],\"schema\":{\"type\"\ + :\"progress\",\"system\":\"progress\"}},{\"id\":\"comment\",\"name\":\"Comment\"\ + ,\"custom\":false,\"orderable\":true,\"navigable\":false,\"searchable\":true,\"\ + clauseNames\":[\"comment\"],\"schema\":{\"type\":\"comments-page\",\"system\"\ + :\"comment\"}},{\"id\":\"votes\",\"name\":\"Votes\",\"custom\":false,\"orderable\"\ + :false,\"navigable\":true,\"searchable\":false,\"clauseNames\":[\"votes\"\ + ],\"schema\":{\"type\":\"votes\",\"system\":\"votes\"}},{\"id\":\"issuelinks\"\ + ,\"name\":\"Linked Issues\",\"custom\":false,\"orderable\":true,\"navigable\"\ + :true,\"searchable\":true,\"clauseNames\":[],\"schema\":{\"type\":\"array\"\ + ,\"items\":\"issuelinks\",\"system\":\"issuelinks\"}},{\"id\":\"worklog\"\ + ,\"name\":\"Log Work\",\"custom\":false,\"orderable\":true,\"navigable\":false,\"\ + searchable\":true,\"clauseNames\":[],\"schema\":{\"type\":\"array\",\"items\"\ + :\"worklog\",\"system\":\"worklog\"}},{\"id\":\"assignee\",\"name\":\"Assignee\"\ + ,\"custom\":false,\"orderable\":true,\"navigable\":true,\"searchable\":true,\"\ + clauseNames\":[\"assignee\"],\"schema\":{\"type\":\"user\",\"system\":\"assignee\"\ + }},{\"id\":\"updated\",\"name\":\"Updated\",\"custom\":false,\"orderable\"\ + :false,\"navigable\":true,\"searchable\":true,\"clauseNames\":[\"updated\"\ + ,\"updatedDate\"],\"schema\":{\"type\":\"datetime\",\"system\":\"updated\"\ + }},{\"id\":\"status\",\"name\":\"Status\",\"custom\":false,\"orderable\":false,\"\ + navigable\":true,\"searchable\":true,\"clauseNames\":[\"status\"],\"schema\"\ + :{\"type\":\"status\",\"system\":\"status\"}}]"} + headers: + Cache-Control: ['no-cache, no-store, no-transform'] + Content-Security-Policy: [frame-ancestors 'self'] + Content-Type: [application/json;charset=UTF-8] + Date: ['Thu, 04 Apr 2019 11:05:22 GMT'] + Set-Cookie: [JSESSIONID=75D9F9AD2FC39F0BDB4A396E39F23C0E; Path=/; HttpOnly] + Transfer-Encoding: [chunked] + Vary: [User-Agent] + X-AREQUESTID: [665x1195x1] + X-ASEN: [SEN-L13384799] + X-ASESSIONID: [1dg9ys4] + X-AUSERNAME: [gbaconnier] + X-Content-Type-Options: [nosniff] + X-Frame-Options: [SAMEORIGIN] + X-Seraph-LoginReason: [OK] + X-XSS-Protection: [1; mode=block] + content-length: ['9557'] + status: {code: 200, message: ''} +- request: + body: null + headers: + Accept: + - !!binary | + YXBwbGljYXRpb24vanNvbiwqLio7cT0wLjk= + Accept-Encoding: + - !!binary | + Z3ppcCwgZGVmbGF0ZQ== + Cache-Control: + - !!binary | + bm8tY2FjaGU= + Connection: + - !!binary | + a2VlcC1hbGl2ZQ== + Content-Type: + - !!binary | + YXBwbGljYXRpb24vanNvbg== + Cookie: + - !!binary | + YXRsYXNzaWFuLnhzcmYudG9rZW49QllHMy02U1BGLTBVTTEtMkxCT183MGYyZmZmNjZkODhjN2I2 + ZWU3ODI5NmE0Nzg3MWMzNjNkMTdkYzBjX2xpbjsgSlNFU1NJT05JRD03NUQ5RjlBRDJGQzM5RjBC + REI0QTM5NkUzOUYyM0MwRQ== + User-Agent: + - !!binary | + cHl0aG9uLXJlcXVlc3RzLzIuMjEuMA== + X-Atlassian-Token: + - !!binary | + bm8tY2hlY2s= + method: GET + uri: http://jira:8080/rest/api/2/issue/10000?fields=issuetype&fields=project&fields=parent&fields=customfield_10101 + response: + body: {string: '{"expand":"renderedFields,names,schema,operations,editmeta,changelog,versionedRepresentations","id":"10000","self":"http://jira:8080/rest/api/2/issue/10000","key":"TEST-1","fields":{"issuetype":{"self":"http://jira:8080/rest/api/2/issuetype/10000","id":"10000","description":"Created + by Jira Software - do not edit or delete. Issue type for a big user story + that needs to be broken down.","iconUrl":"http://jira:8080/images/icons/issuetypes/epic.svg","name":"Epic","subtask":false},"project":{"self":"http://jira:8080/rest/api/2/project/10000","id":"10000","key":"TEST","name":"TEST","avatarUrls":{"48x48":"http://jira:8080/secure/projectavatar?avatarId=10324","24x24":"http://jira:8080/secure/projectavatar?size=small&avatarId=10324","16x16":"http://jira:8080/secure/projectavatar?size=xsmall&avatarId=10324","32x32":"http://jira:8080/secure/projectavatar?size=medium&avatarId=10324"}},"customfield_10101":null}}'} + headers: + Cache-Control: ['no-cache, no-store, no-transform'] + Content-Security-Policy: [frame-ancestors 'self'] + Content-Type: [application/json;charset=UTF-8] + Date: ['Thu, 04 Apr 2019 11:05:22 GMT'] + Set-Cookie: [JSESSIONID=27FCC00E8328AF6BB7D97B4A10087AB5; Path=/; HttpOnly] + Transfer-Encoding: [chunked] + Vary: [User-Agent] + X-AREQUESTID: [665x1196x1] + X-ASEN: [SEN-L13384799] + X-ASESSIONID: [1me37e8] + X-AUSERNAME: [gbaconnier] + X-Content-Type-Options: [nosniff] + X-Frame-Options: [SAMEORIGIN] + X-Seraph-LoginReason: [OK] + X-XSS-Protection: [1; mode=block] + content-length: ['913'] + status: {code: 200, message: ''} +version: 1 diff --git a/connector_jira/tests/test_auth.py b/connector_jira/tests/test_auth.py new file mode 100644 index 000000000..fffcde99f --- /dev/null +++ b/connector_jira/tests/test_auth.py @@ -0,0 +1,70 @@ +# Copyright 2019 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) + +from odoo import exceptions + +from .common import recorder, JiraTransactionCase + + +class TestAuth(JiraTransactionCase): + + @recorder.use_cassette + def test_auth_oauth(self): + backend = self.backend_record + # reset access + backend.write({ + 'access_token': False, + 'access_secret': False, + }) + self.assertEqual(backend.state, 'authenticate') + auth_wizard = self.env['jira.backend.auth'].create({ + 'backend_id': backend.id, + }) + + self.assertEqual(auth_wizard.state, 'leg_1') + # Here, the wizard generates a consumer key and + # a private/public key. User has to copy them in Jira. + self.assertTrue(auth_wizard.consumer_key) + self.assertTrue(auth_wizard.public_key) + self.assertTrue(auth_wizard.backend_id.private_key) + # Once copied in Jira, they have to confirm "Leg 1" + auth_wizard.do_oauth_leg_1() + + # during leg 1, Jira validates the keys and returns + # an authentication URL that the user has to open + # (will need to login). + # For this test, I manually validated the auth URI, + # as we record the requests, the recorded interactions + # will work for the test. + self.assertTrue(auth_wizard.auth_uri) + self.assertEqual(auth_wizard.state, 'leg_2') + # returned by Jira: + self.assertTrue(auth_wizard.request_token) + self.assertTrue(auth_wizard.request_secret) + + auth_wizard.do_oauth_leg_3() + + # of course, these are dummy tokens recorded for the test + # on a demo Jira + self.assertEqual(backend.access_token, + 'o7XglNpQdA3pwzGZw9r6WA2X2XZcjaaI') + self.assertEqual(backend.access_secret, + 'pwq9Qzc7iax0JtoQqZdLvPlv4ReECZGh') + self.assertEqual(auth_wizard.state, 'done') + + @recorder.use_cassette + def test_auth_check_connection(self): + with self.assertRaisesRegex(exceptions.UserError, + 'Connection successful'): + self.backend_record.check_connection() + + @recorder.use_cassette + def test_auth_check_connection_failure(self): + # reset access + self.backend_record.write({ + 'access_token': False, + 'access_secret': False, + }) + with self.assertRaisesRegex(exceptions.UserError, + 'Failed to connect'): + self.backend_record.check_connection() diff --git a/connector_jira/tests/test_import_account_analytic_line.py b/connector_jira/tests/test_import_account_analytic_line.py new file mode 100644 index 000000000..d88798f73 --- /dev/null +++ b/connector_jira/tests/test_import_account_analytic_line.py @@ -0,0 +1,66 @@ +# Copyright 2019 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) + +from .common import recorder, JiraTransactionCase + + +class TestImportAccountAnalyticLine(JiraTransactionCase): + + @classmethod + def setUpClass(cls): + super().setUpClass() + cls._create_issue_type_bindings() + cls.project = cls.env['project.project'].create({ + 'name': 'Test Project', + }) + cls.project_binding = cls._create_project_binding( + cls.project, + issue_types=cls.env['jira.issue.type'].search([]), + external_id='10000' + ) + cls.epic_issue_type = cls.env['jira.issue.type'].search([ + ('name', '=', 'Epic'), + ]) + cls.task = cls.env['project.task'].create({ + 'name': 'My task', + 'project_id': cls.project.id, + }) + cls._link_user(cls.env.user, 'gbaconnier') + + @recorder.use_cassette + def test_import_worklog(self): + """Import a worklog on a task existing in Odoo""" + self._create_task_binding( + self.task, external_id='10000' + ) + jira_issue_id = '10000' + jira_worklog_id = '10000' + self.env['jira.account.analytic.line'].import_record( + self.backend_record, jira_issue_id, jira_worklog_id + ) + binding = self.env['jira.account.analytic.line'].search([ + ('backend_id', '=', self.backend_record.id), + ('external_id', '=', jira_worklog_id) + ]) + self.assertEqual(len(binding), 1) + + self.assertRecordValues( + binding, + [{ + 'account_id': self.project.analytic_account_id.id, + 'backend_id': self.backend_record.id, + 'date': '2019-04-04', + 'employee_id': self.env.user.employee_ids[0].id, + 'external_id': jira_worklog_id, + 'jira_epic_issue_key': False, + 'jira_issue_id': jira_issue_id, + 'jira_issue_key': 'TEST-1', + 'jira_issue_type_id': self.epic_issue_type.id, + 'name': 'write tests', + 'project_id': self.project.id, + 'tag_ids': [], + 'task_id': self.task.id, + 'unit_amount': 1.0, + 'user_id': self.env.user.id, + }] + ) diff --git a/connector_jira/tests/test_import_issue_type.py b/connector_jira/tests/test_import_issue_type.py new file mode 100644 index 000000000..6372eedfb --- /dev/null +++ b/connector_jira/tests/test_import_issue_type.py @@ -0,0 +1,41 @@ +# Copyright 2019 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) + +from .common import recorder, JiraTransactionCase + + +class TestImportIssueType(JiraTransactionCase): + + @recorder.use_cassette + def test_import_issue_type_batch(self): + issue_types = self.env['jira.issue.type'].search([]) + self.assertEqual(len(issue_types), 0) + self.env['jira.issue.type'].import_batch( + self.backend_record, + ) + issue_types = self.env['jira.issue.type'].search([]) + self.assertEqual(len(issue_types), 5) + + def test_import_is_issue_type_sync(self): + self._create_issue_type_bindings() + + epic_issue_type = self.env['jira.issue.type'].search([ + ('name', '=', 'Epic'), + ]) + task_issue_type = self.env['jira.issue.type'].search([ + ('name', '=', 'Task'), + ]) + + project = self.env['project.project'].create({ + 'name': 'Test Project', + }) + project_binding = self._create_project_binding( + project, issue_types=epic_issue_type, + ) + + self.assertTrue( + epic_issue_type.is_sync_for_project(project_binding) + ) + self.assertFalse( + task_issue_type.is_sync_for_project(project_binding) + ) diff --git a/connector_jira/tests/test_import_task.py b/connector_jira/tests/test_import_task.py new file mode 100644 index 000000000..687fbae9b --- /dev/null +++ b/connector_jira/tests/test_import_task.py @@ -0,0 +1,103 @@ +# Copyright 2019 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) + +from .common import recorder, JiraTransactionCase + + +class TestImportTask(JiraTransactionCase): + + @classmethod + def setUpClass(cls): + super().setUpClass() + cls._create_issue_type_bindings() + cls.epic_issue_type = cls.env['jira.issue.type'].search([ + ('name', '=', 'Epic'), + ]) + cls.task_issue_type = cls.env['jira.issue.type'].search([ + ('name', '=', 'Task'), + ]) + cls.subtask_issue_type = cls.env['jira.issue.type'].search([ + ('name', '=', 'Sub-task'), + ]) + cls.project = cls.env['project.project'].create({ + 'name': 'Test Project', + }) + + @recorder.use_cassette + def test_import_task_epic(self): + """Import Epic task where we sync this type issue""" + self._create_project_binding( + self.project, issue_types=self.epic_issue_type, + external_id='10000' + ) + jira_issue_id = '10000' + self.env['jira.project.task'].import_record( + self.backend_record, jira_issue_id + ) + binding = self.env['jira.project.task'].search([ + ('backend_id', '=', self.backend_record.id), + ('external_id', '=', jira_issue_id) + ]) + self.assertEqual(len(binding), 1) + + self.assertEqual(binding.jira_key, 'TEST-1') + self.assertEqual(binding.jira_issue_type_id, self.epic_issue_type) + self.assertFalse(binding.jira_epic_link_id) + self.assertFalse(binding.jira_parent_id) + + @recorder.use_cassette + def test_import_task_type_not_synced(self): + """Import ask where we do not sync this type issue: ignored""" + self._create_project_binding( + self.project, external_id='10000' + ) + jira_issue_id = '10000' + self.env['jira.project.task'].import_record( + self.backend_record, jira_issue_id + ) + binding = self.env['jira.project.task'].search([ + ('backend_id', '=', self.backend_record.id), + ('external_id', '=', jira_issue_id) + ]) + self.assertEqual(len(binding), 0) + + @recorder.use_cassette + def test_import_task_parents(self): + """Import Epic/Task/Subtask recursively""" + self._create_project_binding( + self.project, + issue_types=( + self.epic_issue_type + + self.task_issue_type + + self.subtask_issue_type + ), + external_id='10000' + ) + jira_subtask_issue_id = '10002' + self.env['jira.project.task'].import_record( + self.backend_record, jira_subtask_issue_id + ) + + binding = self.env['jira.project.task'].search([ + ('backend_id', '=', self.backend_record.id), + ('external_id', '=', jira_subtask_issue_id) + ]) + self.assertEqual(len(binding), 1) + + self.assertEqual(binding.jira_key, 'TEST-3') + self.assertEqual(binding.name, 'Subtask1') + self.assertEqual(binding.jira_issue_type_id, self.subtask_issue_type) + self.assertTrue(binding.jira_parent_id) + + task_binding = binding.jira_parent_id + self.assertEqual(task_binding.jira_key, 'TEST-2') + self.assertEqual(task_binding.name, 'Task1') + self.assertEqual(task_binding.jira_issue_type_id, + self.task_issue_type) + self.assertTrue(task_binding.jira_epic_link_id) + + epic_binding = task_binding.jira_epic_link_id + self.assertEqual(epic_binding.jira_key, 'TEST-1') + self.assertEqual(epic_binding.name, 'Epic1') + self.assertEqual(epic_binding.jira_issue_type_id, + self.epic_issue_type) diff --git a/connector_jira/views/jira_backend_views.xml b/connector_jira/views/jira_backend_views.xml index 8790d7731..c0cc48dc6 100644 --- a/connector_jira/views/jira_backend_views.xml +++ b/connector_jira/views/jira_backend_views.xml @@ -49,6 +49,7 @@ + - - +
+
+
+
diff --git a/connector_jira/views/project_task_views.xml b/connector_jira/views/project_task_views.xml index 7a63bf175..ef406e61d 100644 --- a/connector_jira/views/project_task_views.xml +++ b/connector_jira/views/project_task_views.xml @@ -1,4 +1,9 @@ + @@ -39,7 +44,7 @@ - + @@ -68,11 +73,15 @@ - -
  • -
  • - Link with JIRA -
  • + + + Link with JIRA
    diff --git a/connector_jira/wizards/__init__.py b/connector_jira/wizards/__init__.py index e5b9bc110..7a644728d 100644 --- a/connector_jira/wizards/__init__.py +++ b/connector_jira/wizards/__init__.py @@ -1,3 +1,4 @@ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + from . import jira_backend_auth from . import jira_account_analytic_line_import -from . import project_task_merge_wizard diff --git a/connector_jira/wizards/jira_account_analytic_line_import.py b/connector_jira/wizards/jira_account_analytic_line_import.py index ed8800983..8ad82c5a9 100644 --- a/connector_jira/wizards/jira_account_analytic_line_import.py +++ b/connector_jira/wizards/jira_account_analytic_line_import.py @@ -1,5 +1,5 @@ # Copyright 2016-2019 Camptocamp SA -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). from odoo import api, models diff --git a/connector_jira/wizards/jira_backend_auth.py b/connector_jira/wizards/jira_backend_auth.py index c3e233dd0..abce495e4 100644 --- a/connector_jira/wizards/jira_backend_auth.py +++ b/connector_jira/wizards/jira_backend_auth.py @@ -1,6 +1,6 @@ # Copyright: 2015 LasLabs, Inc. # Copyright 2016-2019 Camptocamp SA -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). import logging diff --git a/connector_jira/wizards/project_task_merge_wizard.py b/connector_jira/wizards/project_task_merge_wizard.py deleted file mode 100644 index 55ba45a03..000000000 --- a/connector_jira/wizards/project_task_merge_wizard.py +++ /dev/null @@ -1,25 +0,0 @@ -# Copyright 2019 Camptocamp SA -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) - -from odoo import api, models, exceptions, _ - - -class ProjectTaskMergeWizard(models.TransientModel): - _inherit = 'project.task.merge.wizard' - - @api.multi - def merge_tasks(self): - self._check_jira_bindings() - result = super().merge_tasks() - self._merge_jira_bindings() - return result - - def _check_jira_bindings(self): - if len(self.mapped('task_ids.jira_bind_ids')) > 1: - raise exceptions.UserError( - _('Merging several tasks coming from JIRA is not allowed.') - ) - - def _merge_jira_bindings(self): - binding = self.mapped('task_ids.jira_bind_ids') - self.target_task_id.jira_bind_ids = binding From 077d338ad2efdf112b8b638d083a1bd32fcba9ba Mon Sep 17 00:00:00 2001 From: Alexey Pelykh Date: Sun, 11 Aug 2019 07:21:02 +0300 Subject: [PATCH 022/113] [IMP] connector_jira: improved user binding - specify key/value when user binding fails - resolve by login (which is unique) first, then my potentially-conflicting email - if multiple JIRA users are returned, merge them by key as it's unique --- connector_jira/i18n/connector_jira.pot | 26 +++++++-------- connector_jira/models/jira_backend/common.py | 7 ++++ connector_jira/models/res_users/common.py | 35 ++++++++++++++------ 3 files changed, 45 insertions(+), 23 deletions(-) diff --git a/connector_jira/i18n/connector_jira.pot b/connector_jira/i18n/connector_jira.pot index db903a50c..d37826f4f 100644 --- a/connector_jira/i18n/connector_jira.pot +++ b/connector_jira/i18n/connector_jira.pot @@ -19,7 +19,7 @@ msgid "# Tasks" msgstr "" #. module: connector_jira -#: code:addons/connector_jira/models/jira_backend/common.py:213 +#: code:addons/connector_jira/models/jira_backend/common.py:214 #: code:addons/connector_jira/models/project_project/common.py:175 #: code:addons/connector_jira/models/project_project/common.py:194 #: code:addons/connector_jira/models/project_project/project_link_jira.py:73 @@ -456,7 +456,7 @@ msgid "Confirm" msgstr "" #. module: connector_jira -#: code:addons/connector_jira/models/jira_backend/common.py:465 +#: code:addons/connector_jira/models/jira_backend/common.py:466 #, python-format msgid "Connection successful" msgstr "" @@ -750,8 +750,8 @@ msgid "External user with limited access, created only for the purpose of sharin msgstr "" #. module: connector_jira -#: code:addons/connector_jira/models/jira_backend/common.py:458 -#: code:addons/connector_jira/models/jira_backend/common.py:462 +#: code:addons/connector_jira/models/jira_backend/common.py:459 +#: code:addons/connector_jira/models/jira_backend/common.py:463 #, python-format msgid "Failed to connect (%s)" msgstr "" @@ -951,7 +951,7 @@ msgid "If the email address is on the blacklist, the contact won't receive mass msgstr "" #. module: connector_jira -#: code:addons/connector_jira/models/jira_backend/common.py:426 +#: code:addons/connector_jira/models/jira_backend/common.py:427 #, python-format msgid "If you change the base URL, you must delete and create the Webhooks again." msgstr "" @@ -1555,7 +1555,7 @@ msgid "Next Activity Type" msgstr "" #. module: connector_jira -#: code:addons/connector_jira/models/res_users/common.py:38 +#: code:addons/connector_jira/models/res_users/common.py:41 #, python-format msgid "No JIRA user could be found" msgstr "" @@ -1683,7 +1683,7 @@ msgid "Only issues of these levels are imported. When a worklog is imported no a msgstr "" #. module: connector_jira -#: code:addons/connector_jira/models/jira_backend/common.py:368 +#: code:addons/connector_jira/models/jira_backend/common.py:369 #, python-format msgid "Only one JIRA backend can use the webhook at a time. You must disable them on the backend \"%s\" before activating them here." msgstr "" @@ -1695,7 +1695,7 @@ msgid "Only one Jira binding can be configured with the Sync. Action \"Export\" msgstr "" #. module: connector_jira -#: code:addons/connector_jira/models/jira_backend/common.py:298 +#: code:addons/connector_jira/models/jira_backend/common.py:299 #, python-format msgid "Only one backend can listen to webhooks" msgstr "" @@ -2142,9 +2142,9 @@ msgid "Several users found (%s) for jira account %s (%s). Please link it manuall msgstr "" #. module: connector_jira -#: code:addons/connector_jira/models/res_users/common.py:64 +#: code:addons/connector_jira/models/res_users/common.py:71 #, python-format -msgid "Several users found for %s. Set it manually." +msgid "Several users found with \"%s\" set to \"%s\". Set it manually." msgstr "" #. module: connector_jira @@ -2357,7 +2357,7 @@ msgid "The JIRA Key is mandatory in order to link a project" msgstr "" #. module: connector_jira -#: code:addons/connector_jira/models/jira_backend/common.py:380 +#: code:addons/connector_jira/models/jira_backend/common.py:381 #, python-format msgid "The Odoo Webhook base URL must be set." msgstr "" @@ -2428,7 +2428,7 @@ msgid "The project will be created on JIRA in background." msgstr "" #. module: connector_jira -#: code:addons/connector_jira/models/jira_backend/common.py:240 +#: code:addons/connector_jira/models/jira_backend/common.py:241 #, python-format msgid "The synchronization timestamp is currently locked, probably due to an ongoing synchronization." msgstr "" @@ -2599,7 +2599,7 @@ msgid "Visit this URL, authorize and continue" msgstr "" #. module: connector_jira -#: code:addons/connector_jira/models/jira_backend/common.py:428 +#: code:addons/connector_jira/models/jira_backend/common.py:429 #, python-format msgid "Warning" msgstr "" diff --git a/connector_jira/models/jira_backend/common.py b/connector_jira/models/jira_backend/common.py index 67d6960db..f51799ed5 100644 --- a/connector_jira/models/jira_backend/common.py +++ b/connector_jira/models/jira_backend/common.py @@ -1,5 +1,6 @@ # Copyright: 2015 LasLabs, Inc. # Copyright 2016-2019 Camptocamp SA +# Copyright 2019 Brainbean Apps (https://brainbeanapps.com) # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). import binascii @@ -503,6 +504,12 @@ def import_res_users(self): ).render({'backend': self, 'result': bknd_result}) return True + @api.multi + def get_user_resolution_order(self): + """ User resolution should happen by login first as it's unique, while + resolving by email is likely to give false positives """ + return ['login', 'email'] + @api.multi def import_issue_type(self): self.env['jira.issue.type'].import_batch(self) diff --git a/connector_jira/models/res_users/common.py b/connector_jira/models/res_users/common.py index 175311cce..b3b8f1145 100644 --- a/connector_jira/models/res_users/common.py +++ b/connector_jira/models/res_users/common.py @@ -1,5 +1,8 @@ # Copyright 2016-2019 Camptocamp SA -# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +# Copyright 2019 Brainbean Apps (https://brainbeanapps.com) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html) + +from itertools import groupby from odoo import _, api, exceptions, fields, models from odoo.addons.component.core import Component @@ -54,20 +57,24 @@ def link_with_jira(self, backends=None, raise_if_mismatch=False): for user in self: if binder.to_external(user, wrap=True): continue - jira_user = adapter.search(fragment=user.email) - if not jira_user: - jira_user = adapter.search(fragment=user.login) + jira_user = None + for resolve_by in backend.get_user_resolution_order(): + resolve_by_key = resolve_by + resolve_by_value = user[resolve_by] + jira_user = adapter.search(fragment=resolve_by_value) + if jira_user: + break if not jira_user: continue elif len(jira_user) > 1: if raise_if_mismatch: raise exceptions.UserError(_( - 'Several users found for %s. ' + 'Several users found with "%s" set to "%s". ' 'Set it manually.' - ) % user.login) + ) % (resolve_by_key, resolve_by_value)) bknd_result['error'].append({ - 'key': 'login', - 'value': user.login, + 'key': resolve_by_key, + 'value': resolve_by_value, 'error': 'multiple_found', 'detail': [x.key for x in jira_user] }) @@ -82,8 +89,8 @@ def link_with_jira(self, backends=None, raise_if_mismatch=False): ]) if existing: bknd_result['error'].append({ - 'key': 'login', - 'value': user.login, + 'key': resolve_by_key, + 'value': resolve_by_value, 'error': 'other_user_bound', 'detail': 'linked with %s' % (existing.login,) }) @@ -127,4 +134,12 @@ def search(self, fragment=None): users = self.client.search_users(fragment, maxResults=None, includeActive=True, includeInactive=True) + + # User 'key' is unique and if same key appears several times, it means + # that same user is present in multiple User Directories + users = list(map( + lambda group: list(group[1])[0], + groupby(users, key=lambda user: user.key) + )) + return users From 2fba13dac2fd9f5e9f03678a0973b922cdc97f2a Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Tue, 20 Aug 2019 07:54:21 +0000 Subject: [PATCH 023/113] connector_jira 12.0.1.0.1 --- connector_jira/__manifest__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/connector_jira/__manifest__.py b/connector_jira/__manifest__.py index 6a440ca47..178b563b5 100644 --- a/connector_jira/__manifest__.py +++ b/connector_jira/__manifest__.py @@ -2,7 +2,7 @@ { 'name': 'JIRA Connector', - 'version': '12.0.1.0.0', + 'version': '12.0.1.0.1', 'author': 'Camptocamp,Odoo Community Association (OCA)', 'license': 'AGPL-3', 'category': 'Connector', From 7b5efb7df9cf21072ac43c36a70e19dd52caa660 Mon Sep 17 00:00:00 2001 From: Alexey Pelykh Date: Sun, 11 Aug 2019 08:22:22 +0300 Subject: [PATCH 024/113] [IMP] connector_jira: do not track sync updates --- connector_jira/components/importer.py | 15 ++++++++++++--- connector_jira/i18n/connector_jira.pot | 10 +++++----- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/connector_jira/components/importer.py b/connector_jira/components/importer.py index e6ab50a54..d952b80ef 100644 --- a/connector_jira/components/importer.py +++ b/connector_jira/components/importer.py @@ -1,4 +1,5 @@ # Copyright 2016-2019 Camptocamp SA +# Copyright 2019 Brainbean Apps (https://brainbeanapps.com) # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). """ @@ -189,7 +190,8 @@ def _retry_unique_violation(self): def _create_context(self): return { - 'connector_no_export': True + 'connector_no_export': True, + 'tracking_disable': True, } def _create(self, data): @@ -198,7 +200,7 @@ def _create(self, data): self._validate_data(data) with self._retry_unique_violation(): model_ctx = self.model.with_context(**self._create_context()) - binding = model_ctx.create(data) + binding = model_ctx.sudo().create(data) _logger.debug('%s created from Jira %s', binding, self.external_id) @@ -211,11 +213,18 @@ def _update_data(self, map_record, **kwargs): **kwargs ) + def _update_context(self): + return { + 'connector_no_export': True, + 'tracking_disable': True, + } + def _update(self, binding, data): """ Update an Odoo record """ # special check on data before import self._validate_data(data) - binding.with_context(connector_no_export=True).write(data) + binding_ctx = binding.with_context(**self._update_context()) + binding_ctx.sudo().write(data) _logger.debug('%s updated from Jira %s', binding, self.external_id) return diff --git a/connector_jira/i18n/connector_jira.pot b/connector_jira/i18n/connector_jira.pot index d37826f4f..5be060009 100644 --- a/connector_jira/i18n/connector_jira.pot +++ b/connector_jira/i18n/connector_jira.pot @@ -185,7 +185,7 @@ msgid "Allow timesheets" msgstr "" #. module: connector_jira -#: code:addons/connector_jira/components/importer.py:345 +#: code:addons/connector_jira/components/importer.py:354 #, python-format msgid "Already up-to-date." msgstr "" @@ -280,13 +280,13 @@ msgid "Batch from {} UTC to {} UTC generated {} delete jobs" msgstr "" #. module: connector_jira -#: code:addons/connector_jira/components/importer.py:458 +#: code:addons/connector_jira/components/importer.py:467 #, python-format msgid "Batch from {} UTC to {} UTC generated {} imports" msgstr "" #. module: connector_jira -#: code:addons/connector_jira/components/importer.py:514 +#: code:addons/connector_jira/components/importer.py:523 #, python-format msgid "Binding not found" msgstr "" @@ -1980,13 +1980,13 @@ msgid "Record Thread ID" msgstr "" #. module: connector_jira -#: code:addons/connector_jira/components/importer.py:524 +#: code:addons/connector_jira/components/importer.py:533 #, python-format msgid "Record deleted" msgstr "" #. module: connector_jira -#: code:addons/connector_jira/components/importer.py:263 +#: code:addons/connector_jira/components/importer.py:272 #: code:addons/connector_jira/models/account_analytic_line/importer.py:290 #, python-format msgid "Record does no longer exist in Jira" From e7fa9931029e33206dddb21a2e05c97c412da6d6 Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Tue, 20 Aug 2019 19:01:05 +0000 Subject: [PATCH 025/113] connector_jira 12.0.1.0.2 --- connector_jira/__manifest__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/connector_jira/__manifest__.py b/connector_jira/__manifest__.py index 178b563b5..b1e478525 100644 --- a/connector_jira/__manifest__.py +++ b/connector_jira/__manifest__.py @@ -2,7 +2,7 @@ { 'name': 'JIRA Connector', - 'version': '12.0.1.0.1', + 'version': '12.0.1.0.2', 'author': 'Camptocamp,Odoo Community Association (OCA)', 'license': 'AGPL-3', 'category': 'Connector', From 9376bb5a4c26a8750a1dd478aede70cf2f149d85 Mon Sep 17 00:00:00 2001 From: Alexey Pelykh Date: Mon, 12 Aug 2019 00:58:19 +0300 Subject: [PATCH 026/113] [IMP] connector_jira: view JIRA keys on project and task views --- .../views/project_project_views.xml | 25 +++++++++++++++++++ connector_jira/views/project_task_views.xml | 11 ++++++++ 2 files changed, 36 insertions(+) diff --git a/connector_jira/views/project_project_views.xml b/connector_jira/views/project_project_views.xml index c7c5798b9..9cd6c6679 100644 --- a/connector_jira/views/project_project_views.xml +++ b/connector_jira/views/project_project_views.xml @@ -80,6 +80,31 @@
    + + project.project.tree + project.project + + + + + + + + + + project.project.kanban + project.project + + + + + + + [] + + + + jira.project.project.tree jira.project.project diff --git a/connector_jira/views/project_task_views.xml b/connector_jira/views/project_task_views.xml index ef406e61d..5bd570cb8 100644 --- a/connector_jira/views/project_task_views.xml +++ b/connector_jira/views/project_task_views.xml @@ -33,6 +33,17 @@
    + + project.task.tree + project.task + + + + + + + + project.task.search.form project.task From c1fa545f23a2742bc8959be1eabe8aa4ac5f73a5 Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Fri, 23 Aug 2019 07:13:28 +0000 Subject: [PATCH 027/113] connector_jira 12.0.1.1.0 --- connector_jira/__manifest__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/connector_jira/__manifest__.py b/connector_jira/__manifest__.py index b1e478525..efa03265b 100644 --- a/connector_jira/__manifest__.py +++ b/connector_jira/__manifest__.py @@ -2,7 +2,7 @@ { 'name': 'JIRA Connector', - 'version': '12.0.1.0.2', + 'version': '12.0.1.1.0', 'author': 'Camptocamp,Odoo Community Association (OCA)', 'license': 'AGPL-3', 'category': 'Connector', From 4a76bd335b67c364c68d4b5ed8f7329de540dda1 Mon Sep 17 00:00:00 2001 From: Alexey Pelykh Date: Sun, 11 Aug 2019 09:17:53 +0300 Subject: [PATCH 028/113] [IMP] connector_jira: restrict linked tasks alteration --- connector_jira/components/importer.py | 2 + connector_jira/i18n/connector_jira.pot | 38 ++++++++--- .../models/project_project/common.py | 7 +++ connector_jira/models/project_task/common.py | 63 +++++++++++++++++++ .../tests/test_import_analytic_line.py | 8 +-- connector_jira/tests/test_import_task.py | 21 +++++++ 6 files changed, 125 insertions(+), 14 deletions(-) diff --git a/connector_jira/components/importer.py b/connector_jira/components/importer.py index d952b80ef..b1838eeb6 100644 --- a/connector_jira/components/importer.py +++ b/connector_jira/components/importer.py @@ -190,6 +190,7 @@ def _retry_unique_violation(self): def _create_context(self): return { + 'connector_jira': True, 'connector_no_export': True, 'tracking_disable': True, } @@ -215,6 +216,7 @@ def _update_data(self, map_record, **kwargs): def _update_context(self): return { + 'connector_jira': True, 'connector_no_export': True, 'tracking_disable': True, } diff --git a/connector_jira/i18n/connector_jira.pot b/connector_jira/i18n/connector_jira.pot index 5be060009..545cada31 100644 --- a/connector_jira/i18n/connector_jira.pot +++ b/connector_jira/i18n/connector_jira.pot @@ -39,7 +39,7 @@ msgid "error:" msgstr "" #. module: connector_jira -#: code:addons/connector_jira/models/project_task/common.py:61 +#: code:addons/connector_jira/models/project_task/common.py:66 #, python-format msgid "A Jira task cannot be deleted." msgstr "" @@ -185,7 +185,7 @@ msgid "Allow timesheets" msgstr "" #. module: connector_jira -#: code:addons/connector_jira/components/importer.py:354 +#: code:addons/connector_jira/components/importer.py:356 #, python-format msgid "Already up-to-date." msgstr "" @@ -280,13 +280,13 @@ msgid "Batch from {} UTC to {} UTC generated {} delete jobs" msgstr "" #. module: connector_jira -#: code:addons/connector_jira/components/importer.py:467 +#: code:addons/connector_jira/components/importer.py:469 #, python-format msgid "Batch from {} UTC to {} UTC generated {} imports" msgstr "" #. module: connector_jira -#: code:addons/connector_jira/components/importer.py:523 +#: code:addons/connector_jira/components/importer.py:525 #, python-format msgid "Binding not found" msgstr "" @@ -739,7 +739,7 @@ msgid "Export to JIRA" msgstr "" #. module: connector_jira -#: code:addons/connector_jira/models/project_project/common.py:228 +#: code:addons/connector_jira/models/project_project/common.py:235 #, python-format msgid "Exported project cannot be deleted." msgstr "" @@ -1908,7 +1908,7 @@ msgid "Project or issue type is not synchronized." msgstr "" #. module: connector_jira -#: code:addons/connector_jira/models/project_project/common.py:317 +#: code:addons/connector_jira/models/project_project/common.py:324 #, python-format msgid "Project template with key \"%s\" not found." msgstr "" @@ -1980,13 +1980,13 @@ msgid "Record Thread ID" msgstr "" #. module: connector_jira -#: code:addons/connector_jira/components/importer.py:533 +#: code:addons/connector_jira/components/importer.py:535 #, python-format msgid "Record deleted" msgstr "" #. module: connector_jira -#: code:addons/connector_jira/components/importer.py:272 +#: code:addons/connector_jira/components/importer.py:274 #: code:addons/connector_jira/models/account_analytic_line/importer.py:290 #, python-format msgid "Record does no longer exist in Jira" @@ -2324,6 +2324,24 @@ msgstr "" msgid "Task Count" msgstr "" +#. module: connector_jira +#: code:addons/connector_jira/models/project_task/common.py:190 +#, python-format +msgid "Task can not be created in project linked to JIRA!" +msgstr "" + +#. module: connector_jira +#: code:addons/connector_jira/models/project_task/common.py:207 +#, python-format +msgid "Task linked to JIRA Issue can not be deleted!" +msgstr "" + +#. module: connector_jira +#: code:addons/connector_jira/models/project_task/common.py:199 +#, python-format +msgid "Task linked to JIRA Issue can not be modified!" +msgstr "" + #. module: connector_jira #: model:ir.model.fields,field_description:connector_jira.field_jira_project_project__task_ids #: model:ir.model.fields,field_description:connector_jira.field_jira_res_users__task_ids @@ -2351,7 +2369,7 @@ msgid "The 'Epic Name' field on JIRA is a custom field. The name of the field is msgstr "" #. module: connector_jira -#: code:addons/connector_jira/models/project_project/common.py:221 +#: code:addons/connector_jira/models/project_project/common.py:228 #, python-format msgid "The JIRA Key is mandatory in order to link a project" msgstr "" @@ -2417,7 +2435,7 @@ msgid "The project is now linked with JIRA." msgstr "" #. module: connector_jira -#: code:addons/connector_jira/models/project_project/common.py:208 +#: code:addons/connector_jira/models/project_project/common.py:215 #, python-format msgid "The project template cannot be modified." msgstr "" diff --git a/connector_jira/models/project_project/common.py b/connector_jira/models/project_project/common.py index a8a1a70d7..c15a20ee9 100644 --- a/connector_jira/models/project_project/common.py +++ b/connector_jira/models/project_project/common.py @@ -195,6 +195,13 @@ def check_project_template_shared(self): binding.project_template_shared ) + @api.multi + def _is_linked(self): + for project in self: + if project.sync_action == 'link': + return True + return False + @api.model def create(self, values): record = super().create(values) diff --git a/connector_jira/models/project_task/common.py b/connector_jira/models/project_task/common.py index 5c2346661..6af12ef9d 100644 --- a/connector_jira/models/project_task/common.py +++ b/connector_jira/models/project_task/common.py @@ -1,4 +1,5 @@ # Copyright 2016-2019 Camptocamp SA +# Copyright 2019 Brainbean Apps (https://brainbeanapps.com) # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). from odoo import api, fields, models, exceptions, _ @@ -54,6 +55,10 @@ class JiraProjectTask(models.Model): "A binding already exists for this task and this backend."), ] + @api.multi + def _is_linked(self): + return self.mapped('jira_project_bind_id')._is_linked() + @api.multi def unlink(self): if any(self.mapped('external_id')): @@ -160,6 +165,64 @@ def name_get(self): names.append((task_id, name)) return names + @api.model + def _get_connector_jira_fields(self): + return [ + 'jira_bind_ids', + 'name', + 'date_deadline', + 'user_id', + 'description', + 'active', + 'project_id', + 'planned_hours', + 'stage_id', + ] + + @api.model + def _connector_jira_create_validate(self, vals): + ProjectProject = self.env['project.project'] + project_id = vals.get('project_id') + if project_id: + project_id = ProjectProject.sudo().browse(project_id) + if not self.env.context.get('connector_jira') and \ + project_id.mapped('jira_bind_ids')._is_linked(): + raise exceptions.UserError(_( + 'Task can not be created in project linked to JIRA!' + )) + + @api.multi + def _connector_jira_write_validate(self, vals): + if not self.env.context.get('connector_jira') and \ + any(f in self._get_connector_jira_fields() for f in vals) and \ + self.mapped('jira_bind_ids')._is_linked(): + raise exceptions.UserError(_( + 'Task linked to JIRA Issue can not be modified!' + )) + + @api.multi + def _connector_jira_unlink_validate(self): + if not self.env.context.get('connector_jira') and \ + self.mapped('jira_bind_ids')._is_linked(): + raise exceptions.UserError(_( + 'Task linked to JIRA Issue can not be deleted!' + )) + + @api.model + def create(self, vals): + self._connector_jira_create_validate(vals) + return super().create(vals) + + @api.multi + def write(self, vals): + self._connector_jira_write_validate(vals) + return super().write(vals) + + @api.multi + def unlink(self): + self._connector_jira_unlink_validate() + return super().unlink() + class TaskAdapter(Component): _name = 'jira.project.task.adapter' diff --git a/connector_jira/tests/test_import_analytic_line.py b/connector_jira/tests/test_import_analytic_line.py index fb3056c3c..f8f3ddaa9 100644 --- a/connector_jira/tests/test_import_analytic_line.py +++ b/connector_jira/tests/test_import_analytic_line.py @@ -16,6 +16,10 @@ def setUpClass(cls): cls.project = cls.env['project.project'].create({ 'name': 'Test Project', }) + cls.task = cls.env['project.task'].create({ + 'name': 'My task', + 'project_id': cls.project.id, + }) cls.project_binding = cls._create_project_binding( cls.project, issue_types=cls.env['jira.issue.type'].search([]), @@ -24,10 +28,6 @@ def setUpClass(cls): cls.epic_issue_type = cls.env['jira.issue.type'].search([ ('name', '=', 'Epic'), ]) - cls.task = cls.env['project.task'].create({ - 'name': 'My task', - 'project_id': cls.project.id, - }) # Warning: if you add new tests or change the cassettes # you might need to change the username cls._link_user(cls.env.user, 'gbaconnier') diff --git a/connector_jira/tests/test_import_task.py b/connector_jira/tests/test_import_task.py index 89a0cb30d..c2d8ec0b0 100644 --- a/connector_jira/tests/test_import_task.py +++ b/connector_jira/tests/test_import_task.py @@ -1,6 +1,9 @@ # Copyright 2019 Camptocamp SA +# Copyright 2019 Brainbean Apps (https://brainbeanapps.com) # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +from odoo import exceptions + from .common import recorder, JiraSavepointCase @@ -62,6 +65,12 @@ def _test_import_task_epic(self, expected_active): self.assertEqual(binding.odoo_id.active, expected_active) + with self.assertRaises(exceptions.UserError): + binding.odoo_id.active = not expected_active + + with self.assertRaises(exceptions.UserError): + binding.odoo_id.unlink() + @recorder.use_cassette def test_import_task_type_not_synced(self): """Import ask where we do not sync this type issue: ignored""" @@ -118,3 +127,15 @@ def test_import_task_parents(self): self.assertEqual(epic_binding.name, 'Epic1') self.assertEqual(epic_binding.jira_issue_type_id, self.epic_issue_type) + + def test_task_restrict_create(self): + self._create_project_binding( + self.project, + issue_types=self.env['jira.issue.type'].search([]), + external_id='10000' + ) + with self.assertRaises(exceptions.UserError): + self.env['project.task'].create({ + 'name': 'My task', + 'project_id': self.project.id, + }) From bc414aa814749bf546b33ac63731a8fa749ccb0c Mon Sep 17 00:00:00 2001 From: Alexey Pelykh Date: Sat, 24 Aug 2019 09:19:12 +0200 Subject: [PATCH 029/113] [IMP] connector_jira: search by jira keys --- connector_jira/i18n/connector_jira.pot | 10 +++++----- .../models/project_project/common.py | 20 ++++++++++++++++++- connector_jira/models/project_task/common.py | 18 +++++++++++++++++ .../tests/test_batch_timestamp_delete.py | 2 +- .../tests/test_batch_timestamp_import.py | 2 +- .../tests/test_delete_analytic_line.py | 2 +- .../tests/test_import_analytic_line.py | 2 +- .../tests/test_import_issue_type.py | 2 +- connector_jira/tests/test_import_task.py | 10 +++++++++- 9 files changed, 56 insertions(+), 12 deletions(-) diff --git a/connector_jira/i18n/connector_jira.pot b/connector_jira/i18n/connector_jira.pot index 545cada31..a8bddb99e 100644 --- a/connector_jira/i18n/connector_jira.pot +++ b/connector_jira/i18n/connector_jira.pot @@ -39,7 +39,7 @@ msgid "error:" msgstr "" #. module: connector_jira -#: code:addons/connector_jira/models/project_task/common.py:66 +#: code:addons/connector_jira/models/project_task/common.py:67 #, python-format msgid "A Jira task cannot be deleted." msgstr "" @@ -1908,7 +1908,7 @@ msgid "Project or issue type is not synchronized." msgstr "" #. module: connector_jira -#: code:addons/connector_jira/models/project_project/common.py:324 +#: code:addons/connector_jira/models/project_project/common.py:342 #, python-format msgid "Project template with key \"%s\" not found." msgstr "" @@ -2325,19 +2325,19 @@ msgid "Task Count" msgstr "" #. module: connector_jira -#: code:addons/connector_jira/models/project_task/common.py:190 +#: code:addons/connector_jira/models/project_task/common.py:208 #, python-format msgid "Task can not be created in project linked to JIRA!" msgstr "" #. module: connector_jira -#: code:addons/connector_jira/models/project_task/common.py:207 +#: code:addons/connector_jira/models/project_task/common.py:225 #, python-format msgid "Task linked to JIRA Issue can not be deleted!" msgstr "" #. module: connector_jira -#: code:addons/connector_jira/models/project_task/common.py:199 +#: code:addons/connector_jira/models/project_task/common.py:217 #, python-format msgid "Task linked to JIRA Issue can not be modified!" msgstr "" diff --git a/connector_jira/models/project_project/common.py b/connector_jira/models/project_project/common.py index c15a20ee9..120e994d2 100644 --- a/connector_jira/models/project_project/common.py +++ b/connector_jira/models/project_project/common.py @@ -14,8 +14,8 @@ pass # already logged in components/adapter.py from odoo import api, fields, models, exceptions, _, tools - from odoo.addons.component.core import Component +from odoo.osv import expression _logger = logging.getLogger(__name__) @@ -250,6 +250,7 @@ class ProjectProject(models.Model): jira_key = fields.Char( string='JIRA Key', compute='_compute_jira_key', + store=True, ) @api.depends('jira_bind_ids.jira_key') @@ -268,6 +269,23 @@ def name_get(self): names.append((project_id, name)) return names + @api.model + def name_search(self, name='', args=None, operator='ilike', limit=100): + res = super().name_search(name, args, operator, limit) + if not name: + return res + domain = [ + '|', + ('jira_key', '=ilike', name + '%'), + ('id', 'in', [x[0] for x in res]) + ] + if operator in expression.NEGATIVE_TERM_OPERATORS: + domain = ['&', '!'] + domain[1:] + return self.search( + domain + (args or []), + limit=limit, + ).name_get() + @api.multi def create_and_link_jira(self): action_link = self.env.ref('connector_jira.open_project_link_jira') diff --git a/connector_jira/models/project_task/common.py b/connector_jira/models/project_task/common.py index 6af12ef9d..556500d60 100644 --- a/connector_jira/models/project_task/common.py +++ b/connector_jira/models/project_task/common.py @@ -4,6 +4,7 @@ from odoo import api, fields, models, exceptions, _ from odoo.addons.component.core import Component +from odoo.osv import expression class JiraProjectTask(models.Model): @@ -165,6 +166,23 @@ def name_get(self): names.append((task_id, name)) return names + @api.model + def name_search(self, name='', args=None, operator='ilike', limit=100): + res = super().name_search(name, args, operator, limit) + if not name: + return res + domain = [ + '|', + ('jira_compound_key', '=ilike', name + '%'), + ('id', 'in', [x[0] for x in res]) + ] + if operator in expression.NEGATIVE_TERM_OPERATORS: + domain = ['&', '!'] + domain[1:] + return self.search( + domain + (args or []), + limit=limit, + ).name_get() + @api.model def _get_connector_jira_fields(self): return [ diff --git a/connector_jira/tests/test_batch_timestamp_delete.py b/connector_jira/tests/test_batch_timestamp_delete.py index e097b7526..4f054c35f 100644 --- a/connector_jira/tests/test_batch_timestamp_delete.py +++ b/connector_jira/tests/test_batch_timestamp_delete.py @@ -18,7 +18,7 @@ def setUpClass(cls): ('name', '=', 'Epic'), ]) cls.project = cls.env['project.project'].create({ - 'name': 'Test Project', + 'name': 'Jira Project', }) # note: when you are recording tests with VCR, Jira diff --git a/connector_jira/tests/test_batch_timestamp_import.py b/connector_jira/tests/test_batch_timestamp_import.py index d0daa0b49..cfc7546a0 100644 --- a/connector_jira/tests/test_batch_timestamp_import.py +++ b/connector_jira/tests/test_batch_timestamp_import.py @@ -18,7 +18,7 @@ def setUpClass(cls): ('name', '=', 'Epic'), ]) cls.project = cls.env['project.project'].create({ - 'name': 'Test Project', + 'name': 'Jira Project', }) # note: when you are recording tests with VCR, Jira diff --git a/connector_jira/tests/test_delete_analytic_line.py b/connector_jira/tests/test_delete_analytic_line.py index c26296295..a18f6c9b9 100644 --- a/connector_jira/tests/test_delete_analytic_line.py +++ b/connector_jira/tests/test_delete_analytic_line.py @@ -13,7 +13,7 @@ def setUpClass(cls): ('name', '=', 'Epic'), ]) cls.project = cls.env['project.project'].create({ - 'name': 'Test Project', + 'name': 'Jira Project', }) @recorder.use_cassette diff --git a/connector_jira/tests/test_import_analytic_line.py b/connector_jira/tests/test_import_analytic_line.py index f8f3ddaa9..ec7c24a6d 100644 --- a/connector_jira/tests/test_import_analytic_line.py +++ b/connector_jira/tests/test_import_analytic_line.py @@ -14,7 +14,7 @@ def setUpClass(cls): super().setUpClass() cls._create_issue_type_bindings() cls.project = cls.env['project.project'].create({ - 'name': 'Test Project', + 'name': 'Jira Project', }) cls.task = cls.env['project.task'].create({ 'name': 'My task', diff --git a/connector_jira/tests/test_import_issue_type.py b/connector_jira/tests/test_import_issue_type.py index 0e3c1c622..89266d351 100644 --- a/connector_jira/tests/test_import_issue_type.py +++ b/connector_jira/tests/test_import_issue_type.py @@ -27,7 +27,7 @@ def test_import_is_issue_type_sync(self): ]) project = self.env['project.project'].create({ - 'name': 'Test Project', + 'name': 'Jira Project', }) project_binding = self._create_project_binding( project, issue_types=epic_issue_type, diff --git a/connector_jira/tests/test_import_task.py b/connector_jira/tests/test_import_task.py index c2d8ec0b0..6e2e087cf 100644 --- a/connector_jira/tests/test_import_task.py +++ b/connector_jira/tests/test_import_task.py @@ -23,7 +23,7 @@ def setUpClass(cls): ('name', '=', 'Sub-task'), ]) cls.project = cls.env['project.project'].create({ - 'name': 'Test Project', + 'name': 'Jira Project', }) @recorder.use_cassette @@ -99,6 +99,10 @@ def test_import_task_parents(self): ), external_id='10000' ) + + projects_by_name = self.env['project.project'].name_search('TEST') + self.assertEqual(len(projects_by_name), 1) + jira_subtask_issue_id = '10002' self.env['jira.project.task'].import_record( self.backend_record, jira_subtask_issue_id @@ -128,6 +132,10 @@ def test_import_task_parents(self): self.assertEqual(epic_binding.jira_issue_type_id, self.epic_issue_type) + tasks_by_name = self.env['project.task'].name_search('TEST-3') + self.assertEqual(len(tasks_by_name), 1) + self.assertEqual(tasks_by_name[0][0], binding.odoo_id.id) + def test_task_restrict_create(self): self._create_project_binding( self.project, From 278ddfcddea44e74f8fc286da4cb6222eba27db3 Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Mon, 2 Sep 2019 07:11:32 +0000 Subject: [PATCH 030/113] connector_jira 12.0.1.2.0 --- connector_jira/__manifest__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/connector_jira/__manifest__.py b/connector_jira/__manifest__.py index efa03265b..deee0b69d 100644 --- a/connector_jira/__manifest__.py +++ b/connector_jira/__manifest__.py @@ -2,7 +2,7 @@ { 'name': 'JIRA Connector', - 'version': '12.0.1.1.0', + 'version': '12.0.1.2.0', 'author': 'Camptocamp,Odoo Community Association (OCA)', 'license': 'AGPL-3', 'category': 'Connector', From 681ac4f8015e5024fca522f225a88aab3557e577 Mon Sep 17 00:00:00 2001 From: Alexey Pelykh Date: Fri, 23 Aug 2019 09:56:00 +0200 Subject: [PATCH 031/113] [IMP] connector_jira: import estimation and status for tasks --- connector_jira/i18n/connector_jira.pot | 4 +-- .../models/project_task/importer.py | 29 ++++++++++++++++++- .../cassettes/test_import_task_parents.yaml | 2 +- connector_jira/tests/test_import_task.py | 7 +++++ 4 files changed, 38 insertions(+), 4 deletions(-) diff --git a/connector_jira/i18n/connector_jira.pot b/connector_jira/i18n/connector_jira.pot index a8bddb99e..e48b98e3a 100644 --- a/connector_jira/i18n/connector_jira.pot +++ b/connector_jira/i18n/connector_jira.pot @@ -1568,7 +1568,7 @@ msgstr "" #. module: connector_jira #: code:addons/connector_jira/models/account_analytic_line/importer.py:66 -#: code:addons/connector_jira/models/project_task/importer.py:65 +#: code:addons/connector_jira/models/project_task/importer.py:66 #, python-format msgid "No user found with login \"%s\" or email \"%s\".You must create a user or link it manually if the login/email differs." msgstr "" @@ -1902,7 +1902,7 @@ msgid "Project Type" msgstr "" #. module: connector_jira -#: code:addons/connector_jira/models/project_task/importer.py:207 +#: code:addons/connector_jira/models/project_task/importer.py:234 #, python-format msgid "Project or issue type is not synchronized." msgstr "" diff --git a/connector_jira/models/project_task/importer.py b/connector_jira/models/project_task/importer.py index 027c8a2d3..ae0f209c0 100644 --- a/connector_jira/models/project_task/importer.py +++ b/connector_jira/models/project_task/importer.py @@ -1,4 +1,5 @@ # Copyright 2016-2019 Camptocamp SA +# Copyright 2019 Brainbean Apps (https://brainbeanapps.com) # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). from odoo import _ @@ -96,7 +97,7 @@ def epic(self, record): @mapping def parent(self, record): - jira_parent = record['fields'].get('parent', {}) + jira_parent = record['fields'].get('parent') if not jira_parent: return {} jira_parent_id = jira_parent['id'] @@ -108,6 +109,32 @@ def parent(self, record): def backend_id(self, record): return {'backend_id': self.backend_record.id} + @mapping + def status(self, record): + status = record['fields'].get('status', {}) + status_name = status.get('name') + if not status_name: + return {'stage_id': False} + project_binder = self.binder_for('jira.project.project') + project_id = project_binder.unwrap_binding( + self.options.project_binding + ) + stage = self.env['project.task.type'].search( + [ + ('name', '=', status_name), + ('project_ids', '=', project_id.id), + ], + limit=1, + ) + return {'stage_id': stage.id} + + @mapping + def time_estimate(self, record): + original_estimate = record['fields'].get('timeoriginalestimate') + if not original_estimate: + return {'planned_hours': False} + return {'planned_hours': float(original_estimate) / 3600.0} + def finalize(self, map_record, values): values = values.copy() if values.get('odoo_id'): diff --git a/connector_jira/tests/fixtures/cassettes/test_import_task_parents.yaml b/connector_jira/tests/fixtures/cassettes/test_import_task_parents.yaml index 290abed30..a71503465 100644 --- a/connector_jira/tests/fixtures/cassettes/test_import_task_parents.yaml +++ b/connector_jira/tests/fixtures/cassettes/test_import_task_parents.yaml @@ -656,7 +656,7 @@ interactions: body: {string: '{"expand":"renderedFields,names,schema,operations,editmeta,changelog,versionedRepresentations","id":"10001","self":"http://jira:8080/rest/api/2/issue/10001","key":"TEST-2","fields":{"issuetype":{"self":"http://jira:8080/rest/api/2/issuetype/10002","id":"10002","description":"A task that needs to be done.","iconUrl":"http://jira:8080/secure/viewavatar?size=xsmall&avatarId=10318&avatarType=issuetype","name":"Task","subtask":false,"avatarId":10318},"timespent":null,"project":{"self":"http://jira:8080/rest/api/2/project/10000","id":"10000","key":"TEST","name":"TEST","avatarUrls":{"48x48":"http://jira:8080/secure/projectavatar?avatarId=10324","24x24":"http://jira:8080/secure/projectavatar?size=small&avatarId=10324","16x16":"http://jira:8080/secure/projectavatar?size=xsmall&avatarId=10324","32x32":"http://jira:8080/secure/projectavatar?size=medium&avatarId=10324"}},"fixVersions":[],"aggregatetimespent":null,"resolution":null,"customfield_10105":"0|i00007:","resolutiondate":null,"workratio":-1,"lastViewed":"2019-04-08T13:51:10.599+0000","watches":{"self":"http://jira:8080/rest/api/2/issue/TEST-2/watchers","watchCount":1,"isWatching":true},"created":"2019-04-04T09:58:51.611+0000","priority":{"self":"http://jira:8080/rest/api/2/priority/3","iconUrl":"http://jira:8080/images/icons/priorities/medium.svg","name":"Medium","id":"3"},"customfield_10100":null,"customfield_10101":"TEST-1","labels":[],"timeestimate":null,"aggregatetimeoriginalestimate":null,"versions":[],"issuelinks":[],"assignee":null,"updated":"2019-04-04T09:58:51.785+0000","status":{"self":"http://jira:8080/rest/api/2/status/10000","description":"","iconUrl":"http://jira:8080/","name":"To Do","id":"10000","statusCategory":{"self":"http://jira:8080/rest/api/2/statuscategory/2","id":2,"key":"new","colorName":"blue-gray","name":"To - Do"}},"components":[],"timeoriginalestimate":null,"description":"my task","timetracking":{},"customfield_10203":null,"customfield_10204":null,"customfield_10205":null,"customfield_10206":null,"attachment":[],"customfield_10207":null,"aggregatetimeestimate":null,"customfield_10208":null,"summary":"Task1","creator":{"self":"http://jira:8080/rest/api/2/user?username=gbaconnier","name":"gbaconnier","key":"gbaconnier","emailAddress":"guewen.baconnier@camptocamp.com","avatarUrls":{"48x48":"https://www.gravatar.com/avatar/ad252192c3f73885676b7d2e850ad13c?d=mm&s=48","24x24":"https://www.gravatar.com/avatar/ad252192c3f73885676b7d2e850ad13c?d=mm&s=24","16x16":"https://www.gravatar.com/avatar/ad252192c3f73885676b7d2e850ad13c?d=mm&s=16","32x32":"https://www.gravatar.com/avatar/ad252192c3f73885676b7d2e850ad13c?d=mm&s=32"},"displayName":"Guewen + Do"}},"components":[],"timeoriginalestimate":16200,"description":"my task","timetracking":{},"customfield_10203":null,"customfield_10204":null,"customfield_10205":null,"customfield_10206":null,"attachment":[],"customfield_10207":null,"aggregatetimeestimate":null,"customfield_10208":null,"summary":"Task1","creator":{"self":"http://jira:8080/rest/api/2/user?username=gbaconnier","name":"gbaconnier","key":"gbaconnier","emailAddress":"guewen.baconnier@camptocamp.com","avatarUrls":{"48x48":"https://www.gravatar.com/avatar/ad252192c3f73885676b7d2e850ad13c?d=mm&s=48","24x24":"https://www.gravatar.com/avatar/ad252192c3f73885676b7d2e850ad13c?d=mm&s=24","16x16":"https://www.gravatar.com/avatar/ad252192c3f73885676b7d2e850ad13c?d=mm&s=16","32x32":"https://www.gravatar.com/avatar/ad252192c3f73885676b7d2e850ad13c?d=mm&s=32"},"displayName":"Guewen Baconnier","active":true,"timeZone":"GMT"},"subtasks":[{"id":"10002","key":"TEST-3","self":"http://jira:8080/rest/api/2/issue/10002","fields":{"summary":"Subtask1","status":{"self":"http://jira:8080/rest/api/2/status/10000","description":"","iconUrl":"http://jira:8080/","name":"To Do","id":"10000","statusCategory":{"self":"http://jira:8080/rest/api/2/statuscategory/2","id":2,"key":"new","colorName":"blue-gray","name":"To Do"}},"priority":{"self":"http://jira:8080/rest/api/2/priority/3","iconUrl":"http://jira:8080/images/icons/priorities/medium.svg","name":"Medium","id":"3"},"issuetype":{"self":"http://jira:8080/rest/api/2/issuetype/10003","id":"10003","description":"The diff --git a/connector_jira/tests/test_import_task.py b/connector_jira/tests/test_import_task.py index 6e2e087cf..842869b4c 100644 --- a/connector_jira/tests/test_import_task.py +++ b/connector_jira/tests/test_import_task.py @@ -25,6 +25,11 @@ def setUpClass(cls): cls.project = cls.env['project.project'].create({ 'name': 'Jira Project', }) + cls.env['project.task.type'].create({ + 'name': 'To Do', + 'sequence': 1, + 'project_ids': [(4, cls.project.id)], + }) @recorder.use_cassette def test_import_task_epic(self): @@ -64,6 +69,7 @@ def _test_import_task_epic(self, expected_active): self.assertFalse(binding.jira_parent_id) self.assertEqual(binding.odoo_id.active, expected_active) + self.assertEqual(binding.odoo_id.stage_id.name, 'To Do') with self.assertRaises(exceptions.UserError): binding.odoo_id.active = not expected_active @@ -125,6 +131,7 @@ def test_import_task_parents(self): self.assertEqual(task_binding.jira_issue_type_id, self.task_issue_type) self.assertTrue(task_binding.jira_epic_link_id) + self.assertAlmostEqual(task_binding.odoo_id.planned_hours, 4.5) epic_binding = task_binding.jira_epic_link_id self.assertEqual(epic_binding.jira_key, 'TEST-1') From 474a9687282c2774a829d5ad1942cc9759bcc5b7 Mon Sep 17 00:00:00 2001 From: Alexey Pelykh Date: Sun, 1 Sep 2019 16:08:56 +0200 Subject: [PATCH 032/113] [IMP] connector_jira: restrict linked AAL modification --- connector_jira/fields.py | 8 ++ connector_jira/i18n/connector_jira.pot | 33 +++++-- .../models/account_analytic_line/common.py | 88 +++++++++++++++++-- .../models/account_analytic_line/importer.py | 13 ++- connector_jira/models/project_task/common.py | 20 ++++- .../tests/test_delete_analytic_line.py | 8 +- 6 files changed, 146 insertions(+), 24 deletions(-) diff --git a/connector_jira/fields.py b/connector_jira/fields.py index 30d1395f3..ec814e2c4 100644 --- a/connector_jira/fields.py +++ b/connector_jira/fields.py @@ -65,3 +65,11 @@ def convert_to_cache(self, value, record, validate=True): ", not date." % (value, self) ) return self.from_string(value) + + +def normalize_field_value(field, value): + # NOTE: In v13 from_string is likely to go + if hasattr(field, 'from_string') and isinstance(value, str): + from_string = getattr(field, 'from_string') + return from_string(value) + return value diff --git a/connector_jira/i18n/connector_jira.pot b/connector_jira/i18n/connector_jira.pot index e48b98e3a..fb7b58ad5 100644 --- a/connector_jira/i18n/connector_jira.pot +++ b/connector_jira/i18n/connector_jira.pot @@ -39,7 +39,7 @@ msgid "error:" msgstr "" #. module: connector_jira -#: code:addons/connector_jira/models/project_task/common.py:67 +#: code:addons/connector_jira/models/project_task/common.py:69 #, python-format msgid "A Jira task cannot be deleted." msgstr "" @@ -1254,6 +1254,7 @@ msgid "Jira Project" msgstr "" #. module: connector_jira +#: model:ir.model.fields,field_description:connector_jira.field_jira_account_analytic_line__jira_project_bind_id #: model:ir.model.fields,field_description:connector_jira.field_jira_project_task__jira_project_bind_id msgid "Jira Project Bind" msgstr "" @@ -1567,7 +1568,7 @@ msgid "No user found for jira account %s (%s). Please link it manually from the msgstr "" #. module: connector_jira -#: code:addons/connector_jira/models/account_analytic_line/importer.py:66 +#: code:addons/connector_jira/models/account_analytic_line/importer.py:67 #: code:addons/connector_jira/models/project_task/importer.py:66 #, python-format msgid "No user found with login \"%s\" or email \"%s\".You must create a user or link it manually if the login/email differs." @@ -1987,7 +1988,7 @@ msgstr "" #. module: connector_jira #: code:addons/connector_jira/components/importer.py:274 -#: code:addons/connector_jira/models/account_analytic_line/importer.py:290 +#: code:addons/connector_jira/models/account_analytic_line/importer.py:297 #, python-format msgid "Record does no longer exist in Jira" msgstr "" @@ -2325,19 +2326,19 @@ msgid "Task Count" msgstr "" #. module: connector_jira -#: code:addons/connector_jira/models/project_task/common.py:208 +#: code:addons/connector_jira/models/project_task/common.py:210 #, python-format msgid "Task can not be created in project linked to JIRA!" msgstr "" #. module: connector_jira -#: code:addons/connector_jira/models/project_task/common.py:225 +#: code:addons/connector_jira/models/project_task/common.py:237 #, python-format msgid "Task linked to JIRA Issue can not be deleted!" msgstr "" #. module: connector_jira -#: code:addons/connector_jira/models/project_task/common.py:217 +#: code:addons/connector_jira/models/project_task/common.py:229 #, python-format msgid "Task linked to JIRA Issue can not be modified!" msgstr "" @@ -2488,6 +2489,24 @@ msgstr "" msgid "Timesheet Line" msgstr "" +#. module: connector_jira +#: code:addons/connector_jira/models/account_analytic_line/common.py:204 +#, python-format +msgid "Timesheet can not be created in project linked to JIRA!" +msgstr "" + +#. module: connector_jira +#: code:addons/connector_jira/models/account_analytic_line/common.py:231 +#, python-format +msgid "Timesheet linked to JIRA Worklog can not be deleted!" +msgstr "" + +#. module: connector_jira +#: code:addons/connector_jira/models/account_analytic_line/common.py:223 +#, python-format +msgid "Timesheet linked to JIRA Worklog can not be modified!" +msgstr "" + #. module: connector_jira #: model:ir.model.fields,field_description:connector_jira.field_jira_project_task__timesheet_ids msgid "Timesheets" @@ -2721,7 +2740,7 @@ msgid "e.g. https://example.com/jira" msgstr "" #. module: connector_jira -#: code:addons/connector_jira/models/account_analytic_line/importer.py:24 +#: code:addons/connector_jira/models/account_analytic_line/importer.py:25 #, python-format msgid "missing description" msgstr "" diff --git a/connector_jira/models/account_analytic_line/common.py b/connector_jira/models/account_analytic_line/common.py index b48de08c3..9fe990920 100644 --- a/connector_jira/models/account_analytic_line/common.py +++ b/connector_jira/models/account_analytic_line/common.py @@ -1,15 +1,18 @@ # Copyright 2016-2019 Camptocamp SA +# Copyright 2019 Brainbean Apps (https://brainbeanapps.com) # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). import json from collections import namedtuple -from odoo import api, fields, models +from odoo import api, fields, models, exceptions, _ from odoo.addons.queue_job.job import job, related_action from odoo.addons.component.core import Component +from ...fields import normalize_field_value + UpdatedWorklog = namedtuple( 'UpdatedWorklog', @@ -46,6 +49,13 @@ class JiraAccountAnalyticLine(models.Model): # in case we'll need it for an eventual export jira_issue_id = fields.Char() + # As we can have more than one jira binding on a project.project, we store + # to which one a task binding is related. + jira_project_bind_id = fields.Many2one( + comodel_name='jira.project.project', + ondelete='restrict', + ) + # we have to store these fields on the analytic line because # they may be different than the ones on their odoo task: # for instance, we do not import "Tasks" but we import "Epics", @@ -78,6 +88,10 @@ class JiraAccountAnalyticLine(models.Model): "A binding already exists for this line and this backend."), ] + @api.multi + def _is_linked(self): + return self.mapped('jira_project_bind_id')._is_linked() + @api.depends('jira_issue_key', 'jira_epic_issue_key') def _compute_jira_issue_url(self): """Compute the external URL to JIRA.""" @@ -123,31 +137,26 @@ class AccountAnalyticLine(models.Model): jira_issue_key = fields.Char( string='Original JIRA Issue Key', compute='_compute_jira_references', - readonly=True, store=True, ) jira_issue_url = fields.Char( string='Original JIRA issue Link', compute='_compute_jira_references', - readonly=True, ) jira_epic_issue_key = fields.Char( compute='_compute_jira_references', string='Original JIRA Epic Key', - readonly=True, store=True, ) jira_epic_issue_url = fields.Char( string='Original JIRA Epic Link', compute='_compute_jira_references', - readonly=True ) jira_issue_type_id = fields.Many2one( comodel_name='jira.issue.type', string='Original JIRA Issue Type', compute='_compute_jira_references', - readonly=True, store=True ) @@ -171,6 +180,73 @@ def _compute_jira_references(self): record.jira_epic_issue_key = main_binding.jira_epic_issue_key record.jira_epic_issue_url = main_binding.jira_epic_issue_url + @api.model + def _get_connector_jira_fields(self): + return [ + 'jira_bind_ids', + 'project_id', + 'task_id', + 'user_id', + 'employee_id', + 'name', + 'date', + 'unit_amount', + ] + + @api.model + def _connector_jira_create_validate(self, vals): + ProjectProject = self.env['project.project'] + project_id = vals.get('project_id') + if project_id: + project_id = ProjectProject.sudo().browse(project_id) + if not self.env.context.get('connector_jira') and \ + project_id.mapped('jira_bind_ids')._is_linked(): + raise exceptions.UserError(_( + 'Timesheet can not be created in project linked to JIRA!' + )) + + @api.multi + def _connector_jira_write_validate(self, vals): + if not self.env.context.get('connector_jira') and \ + self.mapped('jira_bind_ids')._is_linked(): + normalized_vals = { + field: normalize_field_value(self._fields[field], value) + for field, value in vals.items() + } + for current_vals in self.read( + list(vals.keys()), load='_classic_write'): + for field in self._get_connector_jira_fields(): + if field not in vals: + continue + if normalized_vals[field] == current_vals[field]: + continue + raise exceptions.UserError(_( + 'Timesheet linked to JIRA Worklog can not be modified!' + )) + + @api.multi + def _connector_jira_unlink_validate(self): + if not self.env.context.get('connector_jira') and \ + self.mapped('jira_bind_ids')._is_linked(): + raise exceptions.UserError(_( + 'Timesheet linked to JIRA Worklog can not be deleted!' + )) + + @api.model + def create(self, vals): + self._connector_jira_create_validate(vals) + return super().create(vals) + + @api.multi + def write(self, vals): + self._connector_jira_write_validate(vals) + return super().write(vals) + + @api.multi + def unlink(self): + self._connector_jira_unlink_validate() + return super().unlink() + class WorklogAdapter(Component): _name = 'jira.worklog.adapter' diff --git a/connector_jira/models/account_analytic_line/importer.py b/connector_jira/models/account_analytic_line/importer.py index 9aec65cd2..4700d63d6 100644 --- a/connector_jira/models/account_analytic_line/importer.py +++ b/connector_jira/models/account_analytic_line/importer.py @@ -1,4 +1,5 @@ # Copyright 2016-2019 Camptocamp SA +# Copyright 2019 Brainbean Apps (https://brainbeanapps.com) # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). import logging @@ -86,11 +87,17 @@ def project_and_task(self, record): return {'project_id': self.options.fallback_project.id} project = self.options.project_binding.odoo_id if project: - return {'project_id': project.id} + return { + 'project_id': project.id, + 'jira_project_bind_id': self.options.project_binding.id, + } project = task_binding.project_id - return {'task_id': task_binding.odoo_id.id, - 'project_id': project.id} + return { + 'task_id': task_binding.odoo_id.id, + 'project_id': project.id, + 'jira_project_bind_id': task_binding.jira_project_bind_id.id, + } @mapping def backend_id(self, record): diff --git a/connector_jira/models/project_task/common.py b/connector_jira/models/project_task/common.py index 556500d60..44a9aa8dc 100644 --- a/connector_jira/models/project_task/common.py +++ b/connector_jira/models/project_task/common.py @@ -6,6 +6,8 @@ from odoo.addons.component.core import Component from odoo.osv import expression +from ...fields import normalize_field_value + class JiraProjectTask(models.Model): _name = 'jira.project.task' @@ -212,11 +214,21 @@ def _connector_jira_create_validate(self, vals): @api.multi def _connector_jira_write_validate(self, vals): if not self.env.context.get('connector_jira') and \ - any(f in self._get_connector_jira_fields() for f in vals) and \ self.mapped('jira_bind_ids')._is_linked(): - raise exceptions.UserError(_( - 'Task linked to JIRA Issue can not be modified!' - )) + normalized_vals = { + field: normalize_field_value(self._fields[field], value) + for field, value in vals.items() + } + for current_vals in self.read( + list(vals.keys()), load='_classic_write'): + for field in self._get_connector_jira_fields(): + if field not in vals: + continue + if normalized_vals[field] == current_vals[field]: + continue + raise exceptions.UserError(_( + 'Task linked to JIRA Issue can not be modified!' + )) @api.multi def _connector_jira_unlink_validate(self): diff --git a/connector_jira/tests/test_delete_analytic_line.py b/connector_jira/tests/test_delete_analytic_line.py index a18f6c9b9..a9d162744 100644 --- a/connector_jira/tests/test_delete_analytic_line.py +++ b/connector_jira/tests/test_delete_analytic_line.py @@ -19,10 +19,6 @@ def setUpClass(cls): @recorder.use_cassette def test_delete_analytic_line(self): """Import all deleted worklogs since last timestamp""" - self._create_project_binding( - self.project, issue_types=self.epic_issue_type, - external_id='10000' - ) # Simulate a worklogs we would already have imported and is # deleted in Jira. First create the binding as it would be # in Odoo. @@ -33,6 +29,10 @@ def test_delete_analytic_line(self): 'name': 'A worklog that will be deleted', 'user_id': self.env.user.id, }) + self._create_project_binding( + self.project, issue_types=self.epic_issue_type, + external_id='10000' + ) binding = self._create_analytic_line_binding( line, jira_issue_id='10101', From 51cd2c500f8e7e59255f401d6eaeb6867fc1c9a0 Mon Sep 17 00:00:00 2001 From: Alexey Pelykh Date: Thu, 7 Nov 2019 15:57:03 +0000 Subject: [PATCH 033/113] [FIX] connector_jira: issues can be moved between projects --- connector_jira/i18n/connector_jira.pot | 29 ++++++++++++++++++- .../models/project_task/importer.py | 3 +- 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/connector_jira/i18n/connector_jira.pot b/connector_jira/i18n/connector_jira.pot index fb7b58ad5..3a53dce69 100644 --- a/connector_jira/i18n/connector_jira.pot +++ b/connector_jira/i18n/connector_jira.pot @@ -1007,6 +1007,11 @@ msgstr "" msgid "Industry" msgstr "" +#. module: connector_jira +#: model:ir.model.fields,field_description:connector_jira.field_jira_project_project__inherit_assignments +msgid "Inherit assignments" +msgstr "" + #. module: connector_jira #: model_terms:ir.ui.view,arch_db:connector_jira.view_jira_backend_form msgid "Install Webhooks" @@ -1422,6 +1427,12 @@ msgstr "" msgid "Latest connection" msgstr "" +#. module: connector_jira +#: model:ir.model.fields,field_description:connector_jira.field_jira_account_analytic_line__limit_role_to_assignments +#: model:ir.model.fields,field_description:connector_jira.field_jira_project_project__limit_role_to_assignments +msgid "Limit role to assignments" +msgstr "" + #. module: connector_jira #: model:ir.model,name:connector_jira.model_project_link_jira msgid "Link Project with JIRA" @@ -1885,6 +1896,11 @@ msgstr "" msgid "Project" msgstr "" +#. module: connector_jira +#: model:ir.model.fields,field_description:connector_jira.field_jira_project_project__assignment_ids +msgid "Project Assignments" +msgstr "" + #. module: connector_jira #: model:ir.model.fields,field_description:connector_jira.field_jira_project_project__jira_bind_ids #: model:ir.model.fields,field_description:connector_jira.field_project_project__jira_bind_ids @@ -1903,7 +1919,7 @@ msgid "Project Type" msgstr "" #. module: connector_jira -#: code:addons/connector_jira/models/project_task/importer.py:234 +#: code:addons/connector_jira/models/project_task/importer.py:233 #, python-format msgid "Project or issue type is not synchronized." msgstr "" @@ -2082,6 +2098,11 @@ msgstr "" msgid "Responsible User" msgstr "" +#. module: connector_jira +#: model:ir.model.fields,field_description:connector_jira.field_jira_account_analytic_line__role_id +msgid "Role" +msgstr "" + #. module: connector_jira #: model_terms:ir.ui.view,arch_db:connector_jira.view_jira_backend_form msgid "Run" @@ -2489,6 +2510,12 @@ msgstr "" msgid "Timesheet Line" msgstr "" +#. module: connector_jira +#: model:ir.model.fields,field_description:connector_jira.field_jira_account_analytic_line__is_role_required +#: model:ir.model.fields,field_description:connector_jira.field_jira_project_project__is_timesheet_role_required +msgid "Timesheet Role Required" +msgstr "" + #. module: connector_jira #: code:addons/connector_jira/models/account_analytic_line/common.py:204 #, python-format diff --git a/connector_jira/models/project_task/importer.py b/connector_jira/models/project_task/importer.py index ae0f209c0..86cc3a287 100644 --- a/connector_jira/models/project_task/importer.py +++ b/connector_jira/models/project_task/importer.py @@ -4,7 +4,7 @@ from odoo import _ from odoo.addons.connector.exception import MappingError -from odoo.addons.connector.components.mapper import mapping, only_create +from odoo.addons.connector.components.mapper import mapping from odoo.addons.component.core import Component @@ -73,7 +73,6 @@ def assignee(self, record): def description(self, record): return {'description': record['renderedFields']['description']} - @only_create @mapping def project(self, record): binder = self.binder_for('jira.project.project') From 088a15153d1c324e1453e5001b430e6b1cad7449 Mon Sep 17 00:00:00 2001 From: Alexey Pelykh Date: Fri, 8 Nov 2019 07:25:15 +0000 Subject: [PATCH 034/113] [IMP] connector_jira: worklog/timesheet date timezone --- connector_jira/__manifest__.py | 2 +- connector_jira/components/mapper.py | 22 ++--- connector_jira/i18n/connector_jira.pot | 66 ++++++++++++--- .../models/account_analytic_line/importer.py | 17 +++- connector_jira/models/jira_backend/common.py | 28 +++++++ .../cassettes/test_import_worklog.yaml | 6 +- .../tests/test_import_analytic_line.py | 82 +++++++++++++++++++ connector_jira/views/jira_backend_views.xml | 17 ++++ 8 files changed, 210 insertions(+), 30 deletions(-) diff --git a/connector_jira/__manifest__.py b/connector_jira/__manifest__.py index deee0b69d..aadeecba1 100644 --- a/connector_jira/__manifest__.py +++ b/connector_jira/__manifest__.py @@ -2,7 +2,7 @@ { 'name': 'JIRA Connector', - 'version': '12.0.1.2.0', + 'version': '12.0.1.3.0', 'author': 'Camptocamp,Odoo Community Association (OCA)', 'license': 'AGPL-3', 'category': 'Connector', diff --git a/connector_jira/components/mapper.py b/connector_jira/components/mapper.py index 1b43358da..d1da680c5 100644 --- a/connector_jira/components/mapper.py +++ b/connector_jira/components/mapper.py @@ -74,33 +74,33 @@ def modifier(self, record, to_attr): return modifier -def iso8601_to_local_date(isodate): - """ Returns the local date from an iso8601 date +def iso8601_to_naive_date(isodate): + """ Returns the naive date from an iso8601 date - Keep only the date, when we want to keep only the local date. + Keep only the date, when we want to keep only the naive date. It's safe to extract it directly from the tz-aware timestamp. Example with 2014-10-07T00:34:59+0200: we want 2014-10-07 and not 2014-10-06 that we would have using the timestamp converted to UTC. """ - local_date = isodate[:10] - return datetime.strptime(local_date, '%Y-%m-%d').date() + naive_date = isodate[:10] + return datetime.strptime(naive_date, '%Y-%m-%d').date() -def iso8601_local_date(field): +def iso8601_naive_date(field): """ A modifier intended to be used on the ``direct`` mappings for importers. A JIRA datetime is formatted using the ISO 8601 format. - Returns the local date from an iso8601 datetime. + Returns the naive date from an iso8601 datetime. - Keep only the date, when we want to keep only the local date. + Keep only the date, when we want to keep only the naive date. It's safe to extract it directly from the tz-aware timestamp. Example with 2014-10-07T00:34:59+0200: we want 2014-10-07 and not 2014-10-06 that we would have using the timestamp converted to UTC. Usage:: - direct = [(iso8601_local_date('name'), 'name')] + direct = [(iso8601_naive_date('name'), 'name')] :param field: name of the source field in the record @@ -110,8 +110,8 @@ def modifier(self, record, to_attr): value = record.get(field) if not value: return False - utc_date = iso8601_to_local_date(value) - return fields.Date.to_string(utc_date) + naive_date = iso8601_to_naive_date(value) + return fields.Date.to_string(naive_date) return modifier diff --git a/connector_jira/i18n/connector_jira.pot b/connector_jira/i18n/connector_jira.pot index 3a53dce69..cc87f67c8 100644 --- a/connector_jira/i18n/connector_jira.pot +++ b/connector_jira/i18n/connector_jira.pot @@ -19,7 +19,7 @@ msgid "# Tasks" msgstr "" #. module: connector_jira -#: code:addons/connector_jira/models/jira_backend/common.py:214 +#: code:addons/connector_jira/models/jira_backend/common.py:235 #: code:addons/connector_jira/models/project_project/common.py:175 #: code:addons/connector_jira/models/project_project/common.py:194 #: code:addons/connector_jira/models/project_project/project_link_jira.py:73 @@ -207,6 +207,11 @@ msgstr "" msgid "Analytic Line" msgstr "" +#. module: connector_jira +#: selection:jira.backend,worklog_date_timezone_mode:0 +msgid "As-is (naive)" +msgstr "" + #. module: connector_jira #: model:ir.model.fields,field_description:connector_jira.field_jira_project_task__user_id msgid "Assigned to" @@ -450,13 +455,18 @@ msgstr "" msgid "Configure Epic Link" msgstr "" +#. module: connector_jira +#: model_terms:ir.ui.view,arch_db:connector_jira.view_jira_backend_form +msgid "Configure worklog fields" +msgstr "" + #. module: connector_jira #: model_terms:ir.ui.view,arch_db:connector_jira.view_jira_aa_line_import msgid "Confirm" msgstr "" #. module: connector_jira -#: code:addons/connector_jira/models/jira_backend/common.py:466 +#: code:addons/connector_jira/models/jira_backend/common.py:494 #, python-format msgid "Connection successful" msgstr "" @@ -750,8 +760,8 @@ msgid "External user with limited access, created only for the purpose of sharin msgstr "" #. module: connector_jira -#: code:addons/connector_jira/models/jira_backend/common.py:459 -#: code:addons/connector_jira/models/jira_backend/common.py:463 +#: code:addons/connector_jira/models/jira_backend/common.py:487 +#: code:addons/connector_jira/models/jira_backend/common.py:491 #, python-format msgid "Failed to connect (%s)" msgstr "" @@ -951,7 +961,7 @@ msgid "If the email address is on the blacklist, the contact won't receive mass msgstr "" #. module: connector_jira -#: code:addons/connector_jira/models/jira_backend/common.py:427 +#: code:addons/connector_jira/models/jira_backend/common.py:448 #, python-format msgid "If you change the base URL, you must delete and create the Webhooks again." msgstr "" @@ -1297,6 +1307,7 @@ msgstr "" #. module: connector_jira #: model:ir.model,name:connector_jira.model_jira_res_users #: model_terms:ir.ui.view,arch_db:connector_jira.view_jira_res_users_form +#: selection:jira.backend,worklog_date_timezone_mode:0 msgid "Jira User" msgstr "" @@ -1579,7 +1590,7 @@ msgid "No user found for jira account %s (%s). Please link it manually from the msgstr "" #. module: connector_jira -#: code:addons/connector_jira/models/account_analytic_line/importer.py:67 +#: code:addons/connector_jira/models/account_analytic_line/importer.py:80 #: code:addons/connector_jira/models/project_task/importer.py:66 #, python-format msgid "No user found with login \"%s\" or email \"%s\".You must create a user or link it manually if the login/email differs." @@ -1695,7 +1706,7 @@ msgid "Only issues of these levels are imported. When a worklog is imported no a msgstr "" #. module: connector_jira -#: code:addons/connector_jira/models/jira_backend/common.py:369 +#: code:addons/connector_jira/models/jira_backend/common.py:390 #, python-format msgid "Only one JIRA backend can use the webhook at a time. You must disable them on the backend \"%s\" before activating them here." msgstr "" @@ -1707,7 +1718,7 @@ msgid "Only one Jira binding can be configured with the Sync. Action \"Export\" msgstr "" #. module: connector_jira -#: code:addons/connector_jira/models/jira_backend/common.py:299 +#: code:addons/connector_jira/models/jira_backend/common.py:320 #, python-format msgid "Only one backend can listen to webhooks" msgstr "" @@ -2004,7 +2015,7 @@ msgstr "" #. module: connector_jira #: code:addons/connector_jira/components/importer.py:274 -#: code:addons/connector_jira/models/account_analytic_line/importer.py:297 +#: code:addons/connector_jira/models/account_analytic_line/importer.py:310 #, python-format msgid "Record does no longer exist in Jira" msgstr "" @@ -2224,6 +2235,11 @@ msgstr "" msgid "Small-sized image of this contact. It is automatically resized as a 64x64px image, with aspect ratio preserved. Use this field anywhere a small image is required." msgstr "" +#. module: connector_jira +#: selection:jira.backend,worklog_date_timezone_mode:0 +msgid "Specific" +msgstr "" + #. module: connector_jira #: model:ir.model.fields,help:connector_jira.field_jira_res_users__new_password msgid "Specify a value only when creating a user or if you're changing the user's password, otherwise leave empty. After a change of password, the user has to login again." @@ -2397,7 +2413,7 @@ msgid "The JIRA Key is mandatory in order to link a project" msgstr "" #. module: connector_jira -#: code:addons/connector_jira/models/jira_backend/common.py:381 +#: code:addons/connector_jira/models/jira_backend/common.py:402 #, python-format msgid "The Odoo Webhook base URL must be set." msgstr "" @@ -2468,7 +2484,7 @@ msgid "The project will be created on JIRA in background." msgstr "" #. module: connector_jira -#: code:addons/connector_jira/models/jira_backend/common.py:241 +#: code:addons/connector_jira/models/jira_backend/common.py:262 #, python-format msgid "The synchronization timestamp is currently locked, probably due to an ongoing synchronization." msgstr "" @@ -2663,7 +2679,7 @@ msgid "Visit this URL, authorize and continue" msgstr "" #. module: connector_jira -#: code:addons/connector_jira/models/jira_backend/common.py:429 +#: code:addons/connector_jira/models/jira_backend/common.py:450 #, python-format msgid "Warning" msgstr "" @@ -2751,6 +2767,30 @@ msgstr "" msgid "Worklog Bindings" msgstr "" +#. module: connector_jira +#: model:ir.model.fields,field_description:connector_jira.field_jira_backend__worklog_date_timezone +msgid "Worklog Date Timezone" +msgstr "" + +#. module: connector_jira +#: model:ir.model.fields,field_description:connector_jira.field_jira_backend__worklog_date_timezone_mode +msgid "Worklog Date Timezone Mode" +msgstr "" + +#. module: connector_jira +#: model_terms:ir.ui.view,arch_db:connector_jira.view_jira_backend_form +msgid "Worklog/Timesheet date timezone" +msgstr "" + +#. module: connector_jira +#: model:ir.model.fields,help:connector_jira.field_jira_backend__worklog_date_timezone_mode +msgid "Worklog/Timesheet date timezone modes:\n" +" - As-is (naive): ignore timezone information\n" +" - Jira User: use author's timezone\n" +" - Specific: use pre-configured timezone\n" +"" +msgstr "" + #. module: connector_jira #: model:ir.model.fields,help:connector_jira.field_jira_backend__worklog_fallback_project_id msgid "Worklogs which could not be linked to any project will be created in this project. Worklogs landing in the fallback project can be reassigned to the correct project by: 1. linking the expected project with the Jira one, 2. using 'Refresh Worklogs from Jira' on the timesheet lines." @@ -2767,7 +2807,7 @@ msgid "e.g. https://example.com/jira" msgstr "" #. module: connector_jira -#: code:addons/connector_jira/models/account_analytic_line/importer.py:25 +#: code:addons/connector_jira/models/account_analytic_line/importer.py:26 #, python-format msgid "missing description" msgstr "" diff --git a/connector_jira/models/account_analytic_line/importer.py b/connector_jira/models/account_analytic_line/importer.py index 4700d63d6..3b37e83c6 100644 --- a/connector_jira/models/account_analytic_line/importer.py +++ b/connector_jira/models/account_analytic_line/importer.py @@ -3,13 +3,14 @@ # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). import logging +from pytz import timezone, utc from odoo import _ from odoo.addons.connector.exception import MappingError from odoo.addons.connector.components.mapper import mapping, only_create from odoo.addons.component.core import Component from ...components.mapper import ( - iso8601_local_date, iso8601_to_utc_datetime, whenempty + iso8601_to_naive_date, iso8601_to_utc_datetime, whenempty ) from ...fields import MilliDatetime @@ -23,7 +24,6 @@ class AnalyticLineMapper(Component): direct = [ (whenempty('comment', _('missing description')), 'name'), - (iso8601_local_date('started'), 'date'), ] @only_create @@ -49,6 +49,19 @@ def issue(self, record): refs['jira_epic_issue_key'] = issue['fields'][epic_field_name] return refs + @mapping + def date(self, record): + mode = self.backend_record.worklog_date_timezone_mode + started = record['started'] + if not mode or mode == 'naive': + return {'date': iso8601_to_naive_date(started)} + started = iso8601_to_utc_datetime(started).replace(tzinfo=utc) + if mode == 'user': + tz = timezone(record['author']['timeZone']) + elif mode == 'specific': + tz = timezone(self.backend_record.worklog_date_timezone) + return {'date': started.astimezone(tz).date()} + @mapping def duration(self, record): spent = float(record['timeSpentSeconds']) diff --git a/connector_jira/models/jira_backend/common.py b/connector_jira/models/jira_backend/common.py index f51799ed5..117d1fd0e 100644 --- a/connector_jira/models/jira_backend/common.py +++ b/connector_jira/models/jira_backend/common.py @@ -6,6 +6,7 @@ import binascii import logging import json +import pytz import urllib.parse from contextlib import contextmanager, closing @@ -89,6 +90,26 @@ def _default_consumer_key(self): "project by: 1. linking the expected project with the Jira one, " "2. using 'Refresh Worklogs from Jira' on the timesheet lines." ) + worklog_date_timezone_mode = fields.Selection( + selection=[ + ('naive', 'As-is (naive)'), + ('user', 'Jira User'), + ('specific', 'Specific'), + ], + default='naive', + help=( + 'Worklog/Timesheet date timezone modes:\n' + ' - As-is (naive): ignore timezone information\n' + ' - Jira User: use author\'s timezone\n' + ' - Specific: use pre-configured timezone\n' + ), + ) + worklog_date_timezone = fields.Selection( + selection=lambda self: [(x, x) for x in pytz.all_timezones], + default=( + lambda self: self._context.get('tz') or self.env.user.tz or 'UTC' + ), + ) state = fields.Selection( selection=[('authenticate', 'Authenticate'), ('setup', 'Setup'), @@ -428,6 +449,13 @@ def onchange_odoo_webhook_base_url(self): 'the Webhooks again.') return {'warning': {'title': _('Warning'), 'message': msg}} + @api.onchange('worklog_date_timezone_mode') + def _onchange_worklog_date_import_timezone_mode(self): + for jira_backend in self: + if jira_backend.worklog_date_timezone_mode == 'specific': + continue + jira_backend.worklog_date_timezone = False + @api.multi def delete_webhooks(self): self.ensure_one() diff --git a/connector_jira/tests/fixtures/cassettes/test_import_worklog.yaml b/connector_jira/tests/fixtures/cassettes/test_import_worklog.yaml index 47b9a78b9..fdd2459ae 100644 --- a/connector_jira/tests/fixtures/cassettes/test_import_worklog.yaml +++ b/connector_jira/tests/fixtures/cassettes/test_import_worklog.yaml @@ -311,7 +311,7 @@ interactions: body: {string: '{"expand":"renderedFields,names,schema,operations,editmeta,changelog,versionedRepresentations","id":"10000","self":"http://jira:8080/rest/api/2/issue/10000","key":"TEST-1","fields":{"issuetype":{"self":"http://jira:8080/rest/api/2/issuetype/10000","id":"10000","description":"Created by Jira Software - do not edit or delete. Issue type for a big user story that needs to be broken down.","iconUrl":"http://jira:8080/images/icons/issuetypes/epic.svg","name":"Epic","subtask":false},"timespent":3600,"project":{"self":"http://jira:8080/rest/api/2/project/10000","id":"10000","key":"TEST","name":"TEST","avatarUrls":{"48x48":"http://jira:8080/secure/projectavatar?avatarId=10324","24x24":"http://jira:8080/secure/projectavatar?size=small&avatarId=10324","16x16":"http://jira:8080/secure/projectavatar?size=xsmall&avatarId=10324","32x32":"http://jira:8080/secure/projectavatar?size=medium&avatarId=10324"}},"fixVersions":[],"aggregatetimespent":3600,"resolution":null,"customfield_10104":"ghx-label-1","customfield_10105":"0|hzzzzz:","customfield_10106":null,"resolutiondate":null,"workratio":-1,"lastViewed":"2019-04-08T13:51:13.798+0000","watches":{"self":"http://jira:8080/rest/api/2/issue/TEST-1/watchers","watchCount":1,"isWatching":true},"created":"2019-04-04T09:31:27.779+0000","priority":{"self":"http://jira:8080/rest/api/2/priority/3","iconUrl":"http://jira:8080/images/icons/priorities/medium.svg","name":"Medium","id":"3"},"customfield_10100":null,"customfield_10101":null,"customfield_10102":{"self":"http://jira:8080/rest/api/2/customFieldOption/10000","value":"To - Do","id":"10000"},"labels":[],"customfield_10103":"Epic1","timeestimate":0,"aggregatetimeoriginalestimate":null,"versions":[],"issuelinks":[],"assignee":null,"updated":"2019-04-04T11:01:47.600+0000","status":{"self":"http://jira:8080/rest/api/2/status/10000","description":"","iconUrl":"http://jira:8080/","name":"To + Do","id":"10000"},"labels":[],"customfield_10103":"Epic1","timeestimate":0,"aggregatetimeoriginalestimate":null,"versions":[],"issuelinks":[],"assignee":null,"updated":"2019-04-04T09:01:47.600+0000","status":{"self":"http://jira:8080/rest/api/2/status/10000","description":"","iconUrl":"http://jira:8080/","name":"To Do","id":"10000","statusCategory":{"self":"http://jira:8080/rest/api/2/statuscategory/2","id":2,"key":"new","colorName":"blue-gray","name":"To Do"}},"components":[],"timeoriginalestimate":null,"description":null,"timetracking":{"remainingEstimate":"0m","timeSpent":"1h","remainingEstimateSeconds":0,"timeSpentSeconds":3600},"customfield_10203":null,"customfield_10204":null,"customfield_10205":null,"customfield_10206":null,"attachment":[],"customfield_10207":null,"aggregatetimeestimate":0,"customfield_10208":null,"summary":"Epic1","creator":{"self":"http://jira:8080/rest/api/2/user?username=gbaconnier","name":"gbaconnier","key":"gbaconnier","emailAddress":"guewen.baconnier@camptocamp.com","avatarUrls":{"48x48":"https://www.gravatar.com/avatar/ad252192c3f73885676b7d2e850ad13c?d=mm&s=48","24x24":"https://www.gravatar.com/avatar/ad252192c3f73885676b7d2e850ad13c?d=mm&s=24","16x16":"https://www.gravatar.com/avatar/ad252192c3f73885676b7d2e850ad13c?d=mm&s=16","32x32":"https://www.gravatar.com/avatar/ad252192c3f73885676b7d2e850ad13c?d=mm&s=32"},"displayName":"Guewen Baconnier","active":true,"timeZone":"GMT"},"subtasks":[],"reporter":{"self":"http://jira:8080/rest/api/2/user?username=gbaconnier","name":"gbaconnier","key":"gbaconnier","emailAddress":"guewen.baconnier@camptocamp.com","avatarUrls":{"48x48":"https://www.gravatar.com/avatar/ad252192c3f73885676b7d2e850ad13c?d=mm&s=48","24x24":"https://www.gravatar.com/avatar/ad252192c3f73885676b7d2e850ad13c?d=mm&s=24","16x16":"https://www.gravatar.com/avatar/ad252192c3f73885676b7d2e850ad13c?d=mm&s=16","32x32":"https://www.gravatar.com/avatar/ad252192c3f73885676b7d2e850ad13c?d=mm&s=32"},"displayName":"Guewen @@ -324,7 +324,7 @@ interactions: branch=com.atlassian.jira.plugin.devstatus.rest.SummaryItemBean@7af21e5f[overall=com.atlassian.jira.plugin.devstatus.summary.beans.BranchOverallBean@7a591452[count=0,lastUpdated=,lastUpdatedTimestamp=],byInstanceType={}]},errors=[],configErrors=[]], devSummaryJson={\"cachedValue\":{\"errors\":[],\"configErrors\":[],\"summary\":{\"pullrequest\":{\"overall\":{\"count\":0,\"lastUpdated\":null,\"stateCount\":0,\"state\":\"OPEN\",\"details\":{\"openCount\":0,\"mergedCount\":0,\"declinedCount\":0,\"total\":0},\"open\":true},\"byInstanceType\":{}},\"build\":{\"overall\":{\"count\":0,\"lastUpdated\":null,\"failedBuildCount\":0,\"successfulBuildCount\":0,\"unknownBuildCount\":0},\"byInstanceType\":{}},\"review\":{\"overall\":{\"count\":0,\"lastUpdated\":null,\"stateCount\":0,\"state\":null,\"dueDate\":null,\"overDue\":false,\"completed\":false},\"byInstanceType\":{}},\"deployment-environment\":{\"overall\":{\"count\":0,\"lastUpdated\":null,\"topEnvironments\":[],\"showProjects\":false,\"successfulCount\":0},\"byInstanceType\":{}},\"repository\":{\"overall\":{\"count\":0,\"lastUpdated\":null},\"byInstanceType\":{}},\"branch\":{\"overall\":{\"count\":0,\"lastUpdated\":null},\"byInstanceType\":{}}}},\"isStale\":false}}","aggregateprogress":{"progress":3600,"total":3600,"percent":100},"customfield_10200":null,"customfield_10201":[],"customfield_10202":null,"environment":null,"duedate":null,"progress":{"progress":3600,"total":3600,"percent":100},"comment":{"comments":[],"maxResults":0,"total":0,"startAt":0},"votes":{"self":"http://jira:8080/rest/api/2/issue/TEST-1/votes","votes":0,"hasVoted":false},"worklog":{"startAt":0,"maxResults":20,"total":1,"worklogs":[{"self":"http://jira:8080/rest/api/2/issue/10000/worklog/10000","author":{"self":"http://jira:8080/rest/api/2/user?username=gbaconnier","name":"gbaconnier","key":"gbaconnier","emailAddress":"guewen.baconnier@camptocamp.com","avatarUrls":{"48x48":"https://www.gravatar.com/avatar/ad252192c3f73885676b7d2e850ad13c?d=mm&s=48","24x24":"https://www.gravatar.com/avatar/ad252192c3f73885676b7d2e850ad13c?d=mm&s=24","16x16":"https://www.gravatar.com/avatar/ad252192c3f73885676b7d2e850ad13c?d=mm&s=16","32x32":"https://www.gravatar.com/avatar/ad252192c3f73885676b7d2e850ad13c?d=mm&s=32"},"displayName":"Guewen Baconnier","active":true,"timeZone":"GMT"},"updateAuthor":{"self":"http://jira:8080/rest/api/2/user?username=gbaconnier","name":"gbaconnier","key":"gbaconnier","emailAddress":"guewen.baconnier@camptocamp.com","avatarUrls":{"48x48":"https://www.gravatar.com/avatar/ad252192c3f73885676b7d2e850ad13c?d=mm&s=48","24x24":"https://www.gravatar.com/avatar/ad252192c3f73885676b7d2e850ad13c?d=mm&s=24","16x16":"https://www.gravatar.com/avatar/ad252192c3f73885676b7d2e850ad13c?d=mm&s=16","32x32":"https://www.gravatar.com/avatar/ad252192c3f73885676b7d2e850ad13c?d=mm&s=32"},"displayName":"Guewen - Baconnier","active":true,"timeZone":"GMT"},"comment":"write tests","created":"2019-04-04T11:01:47.597+0000","updated":"2019-04-04T11:01:47.597+0000","started":"2019-04-04T11:01:00.000+0000","timeSpent":"1h","timeSpentSeconds":3600,"id":"10000","issueId":"10000"}]}},"renderedFields":{"issuetype":null,"timespent":"1 + Baconnier","active":true,"timeZone":"GMT"},"comment":"write tests","created":"2019-04-04T04:01:47.597+0800","updated":"2019-04-04T04:01:47.597+0800","started":"2019-04-04T11:01:00.000+0000","timeSpent":"1h","timeSpentSeconds":3600,"id":"10000","issueId":"10000"}]}},"renderedFields":{"issuetype":null,"timespent":"1 hour","project":null,"fixVersions":null,"aggregatetimespent":"1 hour","resolution":null,"customfield_10104":"ghx-label-1","customfield_10105":null,"customfield_10106":null,"resolutiondate":null,"workratio":null,"lastViewed":"4 days ago 1:51 PM","watches":null,"created":"04/Apr/19 9:31 AM","priority":null,"customfield_10100":null,"customfield_10101":null,"customfield_10102":null,"labels":null,"customfield_10103":"Epic1","timeestimate":"0 minutes","aggregatetimeoriginalestimate":null,"versions":null,"issuelinks":null,"assignee":null,"updated":"04/Apr/19 @@ -664,7 +664,7 @@ interactions: response: body: {string: '{"self":"http://jira:8080/rest/api/2/issue/10000/worklog/10000","author":{"self":"http://jira:8080/rest/api/2/user?username=gbaconnier","name":"gbaconnier","key":"gbaconnier","emailAddress":"guewen.baconnier@camptocamp.com","avatarUrls":{"48x48":"https://www.gravatar.com/avatar/ad252192c3f73885676b7d2e850ad13c?d=mm&s=48","24x24":"https://www.gravatar.com/avatar/ad252192c3f73885676b7d2e850ad13c?d=mm&s=24","16x16":"https://www.gravatar.com/avatar/ad252192c3f73885676b7d2e850ad13c?d=mm&s=16","32x32":"https://www.gravatar.com/avatar/ad252192c3f73885676b7d2e850ad13c?d=mm&s=32"},"displayName":"Guewen Baconnier","active":true,"timeZone":"GMT"},"updateAuthor":{"self":"http://jira:8080/rest/api/2/user?username=gbaconnier","name":"gbaconnier","key":"gbaconnier","emailAddress":"guewen.baconnier@camptocamp.com","avatarUrls":{"48x48":"https://www.gravatar.com/avatar/ad252192c3f73885676b7d2e850ad13c?d=mm&s=48","24x24":"https://www.gravatar.com/avatar/ad252192c3f73885676b7d2e850ad13c?d=mm&s=24","16x16":"https://www.gravatar.com/avatar/ad252192c3f73885676b7d2e850ad13c?d=mm&s=16","32x32":"https://www.gravatar.com/avatar/ad252192c3f73885676b7d2e850ad13c?d=mm&s=32"},"displayName":"Guewen - Baconnier","active":true,"timeZone":"GMT"},"comment":"write tests","created":"2019-04-04T11:01:47.597+0000","updated":"2019-04-04T11:01:47.597+0000","started":"2019-04-04T11:01:00.000+0000","timeSpent":"1h","timeSpentSeconds":3600,"id":"10000","issueId":"10000"}'} + Baconnier","active":true,"timeZone":"GMT"},"comment":"write tests","created":"2019-04-04T04:01:47.597+0800","updated":"2019-04-04T04:01:47.597+0800","started":"2019-04-04T04:01:47.597+0800","timeSpent":"1h","timeSpentSeconds":3600,"id":"10000","issueId":"10000"}'} headers: Cache-Control: ['no-cache, no-store, no-transform'] Content-Security-Policy: [frame-ancestors 'self'] diff --git a/connector_jira/tests/test_import_analytic_line.py b/connector_jira/tests/test_import_analytic_line.py index ec7c24a6d..71a0ca60b 100644 --- a/connector_jira/tests/test_import_analytic_line.py +++ b/connector_jira/tests/test_import_analytic_line.py @@ -108,3 +108,85 @@ def _test_import_worklog(self, expected_project, expected_task): 'user_id': self.env.user.id, }] ) + + @recorder.use_cassette('test_import_worklog.yaml') + def test_import_worklog_naive(self): + jira_worklog_id = jira_issue_id = '10000' + self.backend_record.worklog_date_timezone_mode = 'naive' + binding = self._setup_import_worklog( + self.task, jira_issue_id, jira_worklog_id) + self.assertRecordValues( + binding, + [{ + 'account_id': self.project.analytic_account_id.id, + 'backend_id': self.backend_record.id, + 'date': date(2019, 4, 4), + 'employee_id': self.env.user.employee_ids[0].id, + 'external_id': jira_worklog_id, + 'jira_epic_issue_key': False, + 'jira_issue_id': jira_issue_id, + 'jira_issue_key': 'TEST-1', + 'jira_issue_type_id': self.epic_issue_type.id, + 'name': 'write tests', + 'project_id': self.project.id, + 'tag_ids': [], + 'task_id': self.task.id if self.task else False, + 'unit_amount': 1.0, + 'user_id': self.env.user.id, + }] + ) + + @recorder.use_cassette('test_import_worklog.yaml') + def test_import_worklog_user(self): + jira_worklog_id = jira_issue_id = '10000' + self.backend_record.worklog_date_timezone_mode = 'user' + binding = self._setup_import_worklog( + self.task, jira_issue_id, jira_worklog_id) + self.assertRecordValues( + binding, + [{ + 'account_id': self.project.analytic_account_id.id, + 'backend_id': self.backend_record.id, + 'date': date(2019, 4, 3), + 'employee_id': self.env.user.employee_ids[0].id, + 'external_id': jira_worklog_id, + 'jira_epic_issue_key': False, + 'jira_issue_id': jira_issue_id, + 'jira_issue_key': 'TEST-1', + 'jira_issue_type_id': self.epic_issue_type.id, + 'name': 'write tests', + 'project_id': self.project.id, + 'tag_ids': [], + 'task_id': self.task.id if self.task else False, + 'unit_amount': 1.0, + 'user_id': self.env.user.id, + }] + ) + + @recorder.use_cassette('test_import_worklog.yaml') + def test_import_worklog_specific(self): + jira_worklog_id = jira_issue_id = '10000' + self.backend_record.worklog_date_timezone_mode = 'specific' + self.backend_record.worklog_date_timezone = 'Europe/London' + binding = self._setup_import_worklog( + self.task, jira_issue_id, jira_worklog_id) + self.assertRecordValues( + binding, + [{ + 'account_id': self.project.analytic_account_id.id, + 'backend_id': self.backend_record.id, + 'date': date(2019, 4, 3), + 'employee_id': self.env.user.employee_ids[0].id, + 'external_id': jira_worklog_id, + 'jira_epic_issue_key': False, + 'jira_issue_id': jira_issue_id, + 'jira_issue_key': 'TEST-1', + 'jira_issue_type_id': self.epic_issue_type.id, + 'name': 'write tests', + 'project_id': self.project.id, + 'tag_ids': [], + 'task_id': self.task.id if self.task else False, + 'unit_amount': 1.0, + 'user_id': self.env.user.id, + }] + ) diff --git a/connector_jira/views/jira_backend_views.xml b/connector_jira/views/jira_backend_views.xml index 615e35823..9805cb183 100644 --- a/connector_jira/views/jira_backend_views.xml +++ b/connector_jira/views/jira_backend_views.xml @@ -198,6 +198,23 @@ the field will be empty.

    + + + + + +
    +

    + Configure worklog fields +

    + From 057db01653d2b53d036529284478a29271dac1e8 Mon Sep 17 00:00:00 2001 From: Alexey Pelykh Date: Thu, 21 Nov 2019 10:40:03 +0100 Subject: [PATCH 035/113] [FIX] connector_jira: UI layout --- connector_jira/i18n/connector_jira.pot | 2 +- connector_jira/views/jira_backend_views.xml | 48 ++++++++++----------- 2 files changed, 23 insertions(+), 27 deletions(-) diff --git a/connector_jira/i18n/connector_jira.pot b/connector_jira/i18n/connector_jira.pot index cc87f67c8..87b671e96 100644 --- a/connector_jira/i18n/connector_jira.pot +++ b/connector_jira/i18n/connector_jira.pot @@ -2779,7 +2779,7 @@ msgstr "" #. module: connector_jira #: model_terms:ir.ui.view,arch_db:connector_jira.view_jira_backend_form -msgid "Worklog/Timesheet date timezone" +msgid "Worklog date timezone" msgstr "" #. module: connector_jira diff --git a/connector_jira/views/jira_backend_views.xml b/connector_jira/views/jira_backend_views.xml index 9805cb183..52d9de7cf 100644 --- a/connector_jira/views/jira_backend_views.xml +++ b/connector_jira/views/jira_backend_views.xml @@ -46,22 +46,16 @@

    - - - - + + - - - - - - + + + + @@ -155,9 +149,11 @@
    - - - + + + + +
    -

    +

    Webhooks can be created only on one instance of JIRA. When webhooks are activated, each modification of tasks and worklogs are directly transmitted to Odoo.

    - - + + @@ -189,7 +185,7 @@ class="btn-sm btn-link oe_inline" attrs="{'invisible': [('state', '=', 'authenticate')]}"/>
    -

    +

    Activate the synchronization of the Epic Link field. Only on JIRA Software. The field contains the name of the JIRA custom field that contains the Epic Link. @@ -198,20 +194,20 @@ the field will be empty.

    - - + +
    -

    +

    Configure worklog fields

    From 71e4eb294ae1289725ac1faba8cedf7ef804e0e6 Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Tue, 3 Dec 2019 18:56:46 +0000 Subject: [PATCH 036/113] connector_jira 12.0.1.4.0 --- connector_jira/__manifest__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/connector_jira/__manifest__.py b/connector_jira/__manifest__.py index aadeecba1..7e4cab4e1 100644 --- a/connector_jira/__manifest__.py +++ b/connector_jira/__manifest__.py @@ -2,7 +2,7 @@ { 'name': 'JIRA Connector', - 'version': '12.0.1.3.0', + 'version': '12.0.1.4.0', 'author': 'Camptocamp,Odoo Community Association (OCA)', 'license': 'AGPL-3', 'category': 'Connector', From 69cae0aa15d71a0d6c5cf42fdbbce2b63219e131 Mon Sep 17 00:00:00 2001 From: Alexey Pelykh Date: Wed, 4 Dec 2019 09:52:00 +0100 Subject: [PATCH 037/113] [IMP] connector_jira: write only updated values, validate by cache format --- connector_jira/components/importer.py | 32 ++++++++++++++++++- connector_jira/fields.py | 8 ----- connector_jira/i18n/connector_jira.pot | 24 +++++++------- .../models/account_analytic_line/common.py | 23 +++++++------ connector_jira/models/project_task/common.py | 23 +++++++------ .../tests/test_import_analytic_line.py | 19 ++++++++++- 6 files changed, 87 insertions(+), 42 deletions(-) diff --git a/connector_jira/components/importer.py b/connector_jira/components/importer.py index b1838eeb6..83fc5742a 100644 --- a/connector_jira/components/importer.py +++ b/connector_jira/components/importer.py @@ -147,6 +147,29 @@ def _validate_data(self, data): """ return + def _filter_data(self, binding, data): + """Filter values that aren't actually changing""" + binding.ensure_one() + fields = list(data.keys()) + new_values = binding._convert_to_cache( + data, + update=True, + validate=False, + ) + old_values = binding._convert_to_cache( + binding.read( + fields, + load='_classic_write', + )[0], + validate=False, + ) + new_data = {} + for field in fields: + if new_values[field] == old_values[field]: + continue + new_data[field] = data[field] + return new_data + def _get_binding(self): """Return the binding id from the jira id""" return self.binder.to_internal(self.external_id) @@ -223,7 +246,14 @@ def _update_context(self): def _update(self, binding, data): """ Update an Odoo record """ - # special check on data before import + data = self._filter_data(binding, data) + if not data: + _logger.debug( + '%s not updated from Jira %s as nothing changed', + binding, + self.external_id, + ) + return self._validate_data(data) binding_ctx = binding.with_context(**self._update_context()) binding_ctx.sudo().write(data) diff --git a/connector_jira/fields.py b/connector_jira/fields.py index ec814e2c4..30d1395f3 100644 --- a/connector_jira/fields.py +++ b/connector_jira/fields.py @@ -65,11 +65,3 @@ def convert_to_cache(self, value, record, validate=True): ", not date." % (value, self) ) return self.from_string(value) - - -def normalize_field_value(field, value): - # NOTE: In v13 from_string is likely to go - if hasattr(field, 'from_string') and isinstance(value, str): - from_string = getattr(field, 'from_string') - return from_string(value) - return value diff --git a/connector_jira/i18n/connector_jira.pot b/connector_jira/i18n/connector_jira.pot index 87b671e96..719e2c785 100644 --- a/connector_jira/i18n/connector_jira.pot +++ b/connector_jira/i18n/connector_jira.pot @@ -39,7 +39,7 @@ msgid "error:" msgstr "" #. module: connector_jira -#: code:addons/connector_jira/models/project_task/common.py:69 +#: code:addons/connector_jira/models/project_task/common.py:67 #, python-format msgid "A Jira task cannot be deleted." msgstr "" @@ -185,7 +185,7 @@ msgid "Allow timesheets" msgstr "" #. module: connector_jira -#: code:addons/connector_jira/components/importer.py:356 +#: code:addons/connector_jira/components/importer.py:386 #, python-format msgid "Already up-to-date." msgstr "" @@ -285,13 +285,13 @@ msgid "Batch from {} UTC to {} UTC generated {} delete jobs" msgstr "" #. module: connector_jira -#: code:addons/connector_jira/components/importer.py:469 +#: code:addons/connector_jira/components/importer.py:499 #, python-format msgid "Batch from {} UTC to {} UTC generated {} imports" msgstr "" #. module: connector_jira -#: code:addons/connector_jira/components/importer.py:525 +#: code:addons/connector_jira/components/importer.py:555 #, python-format msgid "Binding not found" msgstr "" @@ -2008,13 +2008,13 @@ msgid "Record Thread ID" msgstr "" #. module: connector_jira -#: code:addons/connector_jira/components/importer.py:535 +#: code:addons/connector_jira/components/importer.py:565 #, python-format msgid "Record deleted" msgstr "" #. module: connector_jira -#: code:addons/connector_jira/components/importer.py:274 +#: code:addons/connector_jira/components/importer.py:304 #: code:addons/connector_jira/models/account_analytic_line/importer.py:310 #, python-format msgid "Record does no longer exist in Jira" @@ -2363,19 +2363,19 @@ msgid "Task Count" msgstr "" #. module: connector_jira -#: code:addons/connector_jira/models/project_task/common.py:210 +#: code:addons/connector_jira/models/project_task/common.py:208 #, python-format msgid "Task can not be created in project linked to JIRA!" msgstr "" #. module: connector_jira -#: code:addons/connector_jira/models/project_task/common.py:237 +#: code:addons/connector_jira/models/project_task/common.py:240 #, python-format msgid "Task linked to JIRA Issue can not be deleted!" msgstr "" #. module: connector_jira -#: code:addons/connector_jira/models/project_task/common.py:229 +#: code:addons/connector_jira/models/project_task/common.py:232 #, python-format msgid "Task linked to JIRA Issue can not be modified!" msgstr "" @@ -2533,19 +2533,19 @@ msgid "Timesheet Role Required" msgstr "" #. module: connector_jira -#: code:addons/connector_jira/models/account_analytic_line/common.py:204 +#: code:addons/connector_jira/models/account_analytic_line/common.py:202 #, python-format msgid "Timesheet can not be created in project linked to JIRA!" msgstr "" #. module: connector_jira -#: code:addons/connector_jira/models/account_analytic_line/common.py:231 +#: code:addons/connector_jira/models/account_analytic_line/common.py:234 #, python-format msgid "Timesheet linked to JIRA Worklog can not be deleted!" msgstr "" #. module: connector_jira -#: code:addons/connector_jira/models/account_analytic_line/common.py:223 +#: code:addons/connector_jira/models/account_analytic_line/common.py:226 #, python-format msgid "Timesheet linked to JIRA Worklog can not be modified!" msgstr "" diff --git a/connector_jira/models/account_analytic_line/common.py b/connector_jira/models/account_analytic_line/common.py index 9fe990920..387255ecd 100644 --- a/connector_jira/models/account_analytic_line/common.py +++ b/connector_jira/models/account_analytic_line/common.py @@ -11,8 +11,6 @@ from odoo.addons.component.core import Component -from ...fields import normalize_field_value - UpdatedWorklog = namedtuple( 'UpdatedWorklog', @@ -209,16 +207,21 @@ def _connector_jira_create_validate(self, vals): def _connector_jira_write_validate(self, vals): if not self.env.context.get('connector_jira') and \ self.mapped('jira_bind_ids')._is_linked(): - normalized_vals = { - field: normalize_field_value(self._fields[field], value) - for field, value in vals.items() - } - for current_vals in self.read( - list(vals.keys()), load='_classic_write'): + fields = list(vals.keys()) + new_values = self._convert_to_cache( + vals, + update=True, + validate=False, + ) + for old_values in self.read(fields, load='_classic_write'): + old_values = self._convert_to_cache( + old_values, + validate=False, + ) for field in self._get_connector_jira_fields(): - if field not in vals: + if field not in fields: continue - if normalized_vals[field] == current_vals[field]: + if new_values[field] == old_values[field]: continue raise exceptions.UserError(_( 'Timesheet linked to JIRA Worklog can not be modified!' diff --git a/connector_jira/models/project_task/common.py b/connector_jira/models/project_task/common.py index 44a9aa8dc..34592f231 100644 --- a/connector_jira/models/project_task/common.py +++ b/connector_jira/models/project_task/common.py @@ -6,8 +6,6 @@ from odoo.addons.component.core import Component from odoo.osv import expression -from ...fields import normalize_field_value - class JiraProjectTask(models.Model): _name = 'jira.project.task' @@ -215,16 +213,21 @@ def _connector_jira_create_validate(self, vals): def _connector_jira_write_validate(self, vals): if not self.env.context.get('connector_jira') and \ self.mapped('jira_bind_ids')._is_linked(): - normalized_vals = { - field: normalize_field_value(self._fields[field], value) - for field, value in vals.items() - } - for current_vals in self.read( - list(vals.keys()), load='_classic_write'): + fields = list(vals.keys()) + new_values = self._convert_to_cache( + vals, + update=True, + validate=False, + ) + for old_values in self.read(fields, load='_classic_write'): + old_values = self._convert_to_cache( + old_values, + validate=False, + ) for field in self._get_connector_jira_fields(): - if field not in vals: + if field not in fields: continue - if normalized_vals[field] == current_vals[field]: + if new_values[field] == old_values[field]: continue raise exceptions.UserError(_( 'Task linked to JIRA Issue can not be modified!' diff --git a/connector_jira/tests/test_import_analytic_line.py b/connector_jira/tests/test_import_analytic_line.py index 71a0ca60b..2fc463c78 100644 --- a/connector_jira/tests/test_import_analytic_line.py +++ b/connector_jira/tests/test_import_analytic_line.py @@ -2,7 +2,7 @@ # Copyright 2019 Brainbean Apps (https://brainbeanapps.com) # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). -from datetime import date +from datetime import date, timedelta from .common import recorder, JiraSavepointCase @@ -109,6 +109,23 @@ def _test_import_worklog(self, expected_project, expected_task): }] ) + def test_reimport_worklog(self): + jira_issue_id = '10000' + jira_worklog_id = '10000' + with recorder.use_cassette('test_import_worklog.yaml'): + binding = self._setup_import_worklog( + self.task, + jira_issue_id, + jira_worklog_id, + ) + write_date = binding.write_date - timedelta(seconds=1) + binding.write({ + 'write_date': write_date, + }) + with recorder.use_cassette('test_import_worklog.yaml'): + binding.force_reimport() + self.assertEqual(binding.write_date, write_date) + @recorder.use_cassette('test_import_worklog.yaml') def test_import_worklog_naive(self): jira_worklog_id = jira_issue_id = '10000' From 53ad6e766f76927dc7315cc421c33ff8363937ca Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Thu, 5 Dec 2019 08:14:53 +0000 Subject: [PATCH 038/113] connector_jira 12.0.1.5.0 --- connector_jira/__manifest__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/connector_jira/__manifest__.py b/connector_jira/__manifest__.py index 7e4cab4e1..8920b21ce 100644 --- a/connector_jira/__manifest__.py +++ b/connector_jira/__manifest__.py @@ -2,7 +2,7 @@ { 'name': 'JIRA Connector', - 'version': '12.0.1.4.0', + 'version': '12.0.1.5.0', 'author': 'Camptocamp,Odoo Community Association (OCA)', 'license': 'AGPL-3', 'category': 'Connector', From 39299b519f63fee9d960a243055f15d103370909 Mon Sep 17 00:00:00 2001 From: Alexey Pelykh Date: Wed, 20 Nov 2019 10:35:40 +0100 Subject: [PATCH 039/113] [IMP] connector_jira: forced import option --- connector_jira/components/importer.py | 8 ++--- connector_jira/i18n/connector_jira.pot | 35 +++++++++++++------ .../models/account_analytic_line/deleter.py | 2 +- .../models/account_analytic_line/importer.py | 8 ++--- connector_jira/models/jira_backend/common.py | 8 +++-- connector_jira/models/jira_binding/common.py | 4 +-- .../tests/test_batch_timestamp_import.py | 6 ++-- connector_jira/views/jira_backend_views.xml | 12 +++++++ 8 files changed, 57 insertions(+), 26 deletions(-) diff --git a/connector_jira/components/importer.py b/connector_jira/components/importer.py index 83fc5742a..757ca1251 100644 --- a/connector_jira/components/importer.py +++ b/connector_jira/components/importer.py @@ -484,7 +484,7 @@ class TimestampBatchImporter(AbstractComponent): _inherit = ['base.importer', 'jira.base'] _usage = 'timestamp.batch.importer' - def run(self, timestamp): + def run(self, timestamp, force=False, **kwargs): """Run the synchronization using the timestamp""" original_timestamp_value = timestamp.last_timestamp if not timestamp._lock(): @@ -494,7 +494,7 @@ def run(self, timestamp): timestamp._update_timestamp(next_timestamp_value) - number = self._handle_records(records) + number = self._handle_records(records, force=force) return _('Batch from {} UTC to {} UTC generated {} imports').format( original_timestamp_value, @@ -502,10 +502,10 @@ def run(self, timestamp): number ) - def _handle_records(self, records): + def _handle_records(self, records, force=False): """Handle the records to import and return the number handled""" for record_id in records: - self._import_record(record_id) + self._import_record(record_id, force=force) return len(records) def _handle_lock_failed(self, timestamp): diff --git a/connector_jira/i18n/connector_jira.pot b/connector_jira/i18n/connector_jira.pot index 719e2c785..bd7eb7b03 100644 --- a/connector_jira/i18n/connector_jira.pot +++ b/connector_jira/i18n/connector_jira.pot @@ -19,7 +19,7 @@ msgid "# Tasks" msgstr "" #. module: connector_jira -#: code:addons/connector_jira/models/jira_backend/common.py:235 +#: code:addons/connector_jira/models/jira_backend/common.py:237 #: code:addons/connector_jira/models/project_project/common.py:175 #: code:addons/connector_jira/models/project_project/common.py:194 #: code:addons/connector_jira/models/project_project/project_link_jira.py:73 @@ -466,7 +466,7 @@ msgid "Confirm" msgstr "" #. module: connector_jira -#: code:addons/connector_jira/models/jira_backend/common.py:494 +#: code:addons/connector_jira/models/jira_backend/common.py:496 #, python-format msgid "Connection successful" msgstr "" @@ -760,8 +760,8 @@ msgid "External user with limited access, created only for the purpose of sharin msgstr "" #. module: connector_jira -#: code:addons/connector_jira/models/jira_backend/common.py:487 -#: code:addons/connector_jira/models/jira_backend/common.py:491 +#: code:addons/connector_jira/models/jira_backend/common.py:489 +#: code:addons/connector_jira/models/jira_backend/common.py:493 #, python-format msgid "Failed to connect (%s)" msgstr "" @@ -961,7 +961,7 @@ msgid "If the email address is on the blacklist, the contact won't receive mass msgstr "" #. module: connector_jira -#: code:addons/connector_jira/models/jira_backend/common.py:448 +#: code:addons/connector_jira/models/jira_backend/common.py:450 #, python-format msgid "If you change the base URL, you must delete and create the Webhooks again." msgstr "" @@ -971,11 +971,21 @@ msgstr "" msgid "Image" msgstr "" +#. module: connector_jira +#: model:ir.model.fields,field_description:connector_jira.field_jira_backend__import_analytic_line_force +msgid "Import Analytic Line Force" +msgstr "" + #. module: connector_jira #: model_terms:ir.ui.view,arch_db:connector_jira.view_jira_backend_form msgid "Import Issue Types" msgstr "" +#. module: connector_jira +#: model:ir.model.fields,field_description:connector_jira.field_jira_backend__import_project_task_force +msgid "Import Project Task Force" +msgstr "" + #. module: connector_jira #: model:ir.model.fields,field_description:connector_jira.field_jira_backend__import_project_task_from_date msgid "Import Project Tasks from date" @@ -1706,7 +1716,7 @@ msgid "Only issues of these levels are imported. When a worklog is imported no a msgstr "" #. module: connector_jira -#: code:addons/connector_jira/models/jira_backend/common.py:390 +#: code:addons/connector_jira/models/jira_backend/common.py:392 #, python-format msgid "Only one JIRA backend can use the webhook at a time. You must disable them on the backend \"%s\" before activating them here." msgstr "" @@ -1718,7 +1728,7 @@ msgid "Only one Jira binding can be configured with the Sync. Action \"Export\" msgstr "" #. module: connector_jira -#: code:addons/connector_jira/models/jira_backend/common.py:320 +#: code:addons/connector_jira/models/jira_backend/common.py:322 #, python-format msgid "Only one backend can listen to webhooks" msgstr "" @@ -2413,7 +2423,7 @@ msgid "The JIRA Key is mandatory in order to link a project" msgstr "" #. module: connector_jira -#: code:addons/connector_jira/models/jira_backend/common.py:402 +#: code:addons/connector_jira/models/jira_backend/common.py:404 #, python-format msgid "The Odoo Webhook base URL must be set." msgstr "" @@ -2484,7 +2494,7 @@ msgid "The project will be created on JIRA in background." msgstr "" #. module: connector_jira -#: code:addons/connector_jira/models/jira_backend/common.py:262 +#: code:addons/connector_jira/models/jira_backend/common.py:264 #, python-format msgid "The synchronization timestamp is currently locked, probably due to an ongoing synchronization." msgstr "" @@ -2679,7 +2689,7 @@ msgid "Visit this URL, authorize and continue" msgstr "" #. module: connector_jira -#: code:addons/connector_jira/models/jira_backend/common.py:450 +#: code:addons/connector_jira/models/jira_backend/common.py:452 #, python-format msgid "Warning" msgstr "" @@ -2806,6 +2816,11 @@ msgstr "" msgid "e.g. https://example.com/jira" msgstr "" +#. module: connector_jira +#: model_terms:ir.ui.view,arch_db:connector_jira.view_jira_backend_form +msgid "forced" +msgstr "" + #. module: connector_jira #: code:addons/connector_jira/models/account_analytic_line/importer.py:26 #, python-format diff --git a/connector_jira/models/account_analytic_line/deleter.py b/connector_jira/models/account_analytic_line/deleter.py index 5e5d49158..e60fa01c7 100644 --- a/connector_jira/models/account_analytic_line/deleter.py +++ b/connector_jira/models/account_analytic_line/deleter.py @@ -25,7 +25,7 @@ class AnalyticLineBatchDeleter(Component): _inherit = ['base.synchronizer', 'jira.base'] _usage = 'timestamp.batch.deleter' - def run(self, timestamp): + def run(self, timestamp, **kwargs): """Run the synchronization using the timestamp""" original_timestamp_value = timestamp.last_timestamp if not timestamp._lock(): diff --git a/connector_jira/models/account_analytic_line/importer.py b/connector_jira/models/account_analytic_line/importer.py index 3b37e83c6..d418e2727 100644 --- a/connector_jira/models/account_analytic_line/importer.py +++ b/connector_jira/models/account_analytic_line/importer.py @@ -139,13 +139,13 @@ def _search(self, timestamp): next_timestamp = MilliDatetime.from_timestamp(result.until) return (next_timestamp, self.backend_adapter.yield_read(worklog_ids)) - def _handle_records(self, records): + def _handle_records(self, records, force=False): count = 0 for worklog in records: count += 1 worklog_id = worklog['id'] issue_id = worklog['issueId'] - self._import_record(issue_id, worklog_id) + self._import_record(issue_id, worklog_id, force=force) return count def _filter_update(self, updated_worklogs): @@ -185,10 +185,10 @@ def _filter_update(self, updated_worklogs): worklog_ids.append(worklog_id) return worklog_ids - def _import_record(self, issue_id, worklog_id, **kwargs): + def _import_record(self, issue_id, worklog_id, force=False, **kwargs): """ Delay the import of the records""" self.model.with_delay(**kwargs).import_record( - self.backend_record, issue_id, worklog_id, + self.backend_record, issue_id, worklog_id, force=force, ) diff --git a/connector_jira/models/jira_backend/common.py b/connector_jira/models/jira_backend/common.py index 117d1fd0e..d606226d0 100644 --- a/connector_jira/models/jira_backend/common.py +++ b/connector_jira/models/jira_backend/common.py @@ -164,12 +164,14 @@ def _default_consumer_key(self): inverse='_inverse_import_project_task_from_date', string='Import Project Tasks from date', ) + import_project_task_force = fields.Boolean() import_analytic_line_from_date = fields.Datetime( compute='_compute_last_import_date', inverse='_inverse_import_analytic_line_from_date', string='Import Worklogs from date', ) + import_analytic_line_force = fields.Boolean() delete_analytic_line_from_date = fields.Datetime( compute='_compute_last_import_date', @@ -296,7 +298,7 @@ def _inverse_delete_analytic_line_from_date(self): @api.multi def _run_background_from_date(self, model, from_date_field, - component_usage): + component_usage, force=False): """ Import records from a date Create jobs and update the sync timestamp in a savepoint; if a @@ -310,7 +312,7 @@ def _run_background_from_date(self, model, from_date_field, component_usage, ) self.env[model].with_delay(priority=9).run_batch_timestamp( - self, timestamp + self, timestamp, force=force ) @api.constrains('use_webhooks') @@ -500,6 +502,7 @@ def import_project_task(self): 'jira.project.task', 'import_project_task_from_date', 'timestamp.batch.importer', + force=self.import_project_task_force, ) return True @@ -509,6 +512,7 @@ def import_analytic_line(self): 'jira.account.analytic.line', 'import_analytic_line_from_date', 'timestamp.batch.importer', + force=self.import_analytic_line_force, ) return True diff --git a/connector_jira/models/jira_binding/common.py b/connector_jira/models/jira_binding/common.py index ed9e7d663..0b3c55296 100644 --- a/connector_jira/models/jira_binding/common.py +++ b/connector_jira/models/jira_binding/common.py @@ -42,11 +42,11 @@ def import_batch(self, backend): @job(default_channel='root.connector_jira.import') @api.model - def run_batch_timestamp(self, backend, timestamp): + def run_batch_timestamp(self, backend, timestamp, force=False): """Prepare batch of records""" with backend.work_on(self._name) as work: importer = work.component(usage=timestamp.component_usage) - return importer.run(timestamp) + return importer.run(timestamp, force=force) @job(default_channel='root.connector_jira.import') @related_action(action="related_action_jira_link") diff --git a/connector_jira/tests/test_batch_timestamp_import.py b/connector_jira/tests/test_batch_timestamp_import.py index cfc7546a0..c5f29d2d2 100644 --- a/connector_jira/tests/test_batch_timestamp_import.py +++ b/connector_jira/tests/test_batch_timestamp_import.py @@ -121,9 +121,9 @@ def test_import_batch_timestamp_analytic_line(self): delay_args = delayable.import_record.call_args_list expected = [ # backend, issue_id, worklog_id - ((self.backend_record, '10102', '10100'), {}), - ((self.backend_record, '10100', '10102'), {}), - ((self.backend_record, '10101', '10101'), {}), + ((self.backend_record, '10102', '10100'), {'force': False}), + ((self.backend_record, '10100', '10102'), {'force': False}), + ((self.backend_record, '10101', '10101'), {'force': False}), ] self.assertEqual( sorted([(args, kwargs) for args, kwargs in delay_args]), diff --git a/connector_jira/views/jira_backend_views.xml b/connector_jira/views/jira_backend_views.xml index 52d9de7cf..70602d789 100644 --- a/connector_jira/views/jira_backend_views.xml +++ b/connector_jira/views/jira_backend_views.xml @@ -113,6 +113,12 @@ + + ( + +