Пример #1
0
 def set_jira_fields(self, auth_user, auth_token, jira_url):
     jira = JIRA(jira_url, basic_auth=(auth_user, auth_token))
     jira_fields = {}
     for field in jira.fields():
         jira_fields[field["name"]] = field["id"]
         jira_fields[field["id"]] = field["id"]
     return jira_fields
Пример #2
0
def _get_system_custom_fields(jira: JIRA) -> List:
    system_custom_fields = []
    jira_fields = jira.fields()
    for system_field in jira_fields:
        if system_field['custom']:
            system_custom_fields.append(system_field)
    return system_custom_fields
Пример #3
0
class JiraWrapper():
    _field_lookup = None

    def __init__(self, force_login=False, url=_DEFAULT_URL):
        auth = get_auth('JIRA', force=force_login)
        self.session = JIRA(url, basic_auth=auth)
        save_auth('JIRA', auth)

    def _init_field_lookup(self, force=False):
        if self._field_lookup is None or force:
            self._field_lookup = {}
            for field in self.session.fields():
                self._field_lookup[field['name']] = field['id']

    def field_id(self, name):
        if self._field_lookup is None:
            self._init_field_lookup()
        return self._field_lookup[name]

    def get_issue_field(self, issue, field):
        try:
            return getattr(
                self.session.issue(issue).fields(), self.field_id(field))
        except JIRAError as err:
            logging.warning('%s', err)
            return None

    def vendors_involved(self, issue):
        vendors = self.get_issue_field(issue, 'Vendors involved')
        if vendors is None:
            return []
        return list(map(lambda x: x.value, vendors))
Пример #4
0
def tms_login():
    tms = JIRA(server='https://tms.netcracker.com', basic_auth =('rudu0916', get_pwd()))

    fields = tms.fields()
    setattr(tms, 'sprintfield', [x['id'] for x in fields if x['name'] == 'Sprint'][0])
    setattr(tms, 'epiclinkfield', [x['id'] for x in fields if x['name'] == 'Epic Link'][0])

    return tms
Пример #5
0
class JiraDataExtractor():
    ''' Extracts structural data from Jira for further analysis '''
    options = None
    jira = None
    cfg = None

    def __init__(self):
        # By default, the client will connect to a JIRA instance started from the Atlassian Plugin SDK.
        # See https://developer.atlassian.com/display/DOCS/Installing+the+Atlassian+Plugin+SDK
        # for details.
        self.cfg = PropertiesHandler()

        self.options = {'server': self.cfg.server_url}
        self.jira = JIRA(
            self.options,
            basic_auth=(self.cfg.user,
                        self.cfg.pwd))  # a username/password tuple

    def connect(self):
        '''It connects against the pre configured Jira server.'''
        self.options = {'server': self.cfg.server_url}
        self.jira = JIRA(
            self.options,
            basic_auth=(self.cfg.user,
                        self.cfg.pwd))  # a username/password tuple

    def getSendToCSVFile(self, fileStr):
        '''Sends the String to a file'''
        f = open(
            ".\\xls-export\\" + time.strftime("%Y%m%d") + "-" +
            time.strftime("%H%M%S") + "-jira-export.csv", "wb")
        f.write(fileStr)
        f.close()

    def getAllCustomFields(self, name):
        '''Getting all the current custom fields ID's and dump it to a CSV file for revision.'''
        # Fetch all fields
        allfields = self.jira.fields()
        # Make a map from field name -> field id
        nameMap = {field['name']: field['id'] for field in allfields}

        stringBuffer = StringIO()
        stringBuffer.write("Field Name;Code\n")

        for field in allfields:
            stringBuffer.write(field['name'] + ";" + field['id'] + "\n")

        self.getSendToCSVFile(stringBuffer.getvalue())

        if (name != None):
            try:
                result = nameMap[name]
            except:
                return None

            return result
        else:
            return None
Пример #6
0
def psup_login():
    psup = JIRA(server='https://psup.netcracker.com', basic_auth =('rudu0916', get_pwd()), max_retries=0)

    fields = psup.fields()
    setattr(psup, 'sprintfield', [x['id'] for x in fields if x['name'] == 'Sprint'][0])
    setattr(psup, 'epiclinkfield', [x['id'] for x in fields if x['name'] == 'Epic Link'][0])
    setattr(psup, 'featurelinkfield', [x['id'] for x in fields if x['name'] == 'Feature Link'][0])

    return psup
Пример #7
0
class JiraBaseBackend(ServiceBackend):

    def __init__(self, settings, core_project=None, reporter_field=None, default_issue_type='Task'):
        self.settings = settings
        self.core_project = core_project
        self.reporter_field = reporter_field
        self.default_issue_type = default_issue_type

        self.jira = JIRA(
            server=settings.backend_url,
            options={'verify': False},
            basic_auth=(settings.username, settings.password),
            validate=False)

        if self.reporter_field:
            try:
                self.reporter_field_id = next(
                    f['id'] for f in self.jira.fields() if self.reporter_field in f['clauseNames'])
            except StopIteration:
                raise JiraBackendError("Can't custom field %s" % self.reporter_field)
Пример #8
0
class JiraRetriever:
    def __init__(self, server):
        self.jira = JIRA(server=server)
        self._users = set()

    def retrieve_issues(self, date_from, date_to, projects=None):
        str_from = date_from.strftime("%Y-%m-%d")
        str_to = date_to.strftime("%Y-%m-%d")

        def _internal(start_at):
            logger.info(f"Retrieving more issues starting at {start_at}")
            jql = f"(status != Closed OR (updated >= {str_from} AND updated <= {str_to}))"
            if projects:
                str_prj = ",".join(projects or [])
                jql += f" AND project in ({str_prj})"

            result = self.jira.search_issues(jql, startAt=start_at)
            self._extract_users_from_issues(result)
            return result

        return IssueIterator(_internal, self.jira.issue)

    def _extract_users_from_issues(self, issues):
        for issue in issues:
            fields = issue.raw["fields"]
            self._users.add((fields.get("assignee") or {}).get("key", None))
            self._users.add((fields.get("creator") or {}).get("key", None))

    def users(self):
        for user in self._users:
            if user is None:
                continue
            logger.info(f"Processing user {user}")
            yield self.jira.user(user, expand=["groups", "applicationRoles"])

    def fields(self):
        for f in self.jira.fields():
            logger.info(f"Processing custom field {f.get('key')}")
            yield f
Пример #9
0
def onProcess(ctxt, dr):
    # First Bit here is pilot required, probably#
    context = pilotpython.Context(ctxt)
    data = pilotpython.DataRecord(dr)
    props = data.getProperties()
    # instatiate jira
    jira = JIRA(server='server', basic_auth=('$(username)', '$(password)'))
    issues = jira.fields()
    jiracols = {}
    for i in issues:
        # print(i['name']+"_"+i['id'])
        jiracols[i['name']] = i['id']

    # This bit pushes out the data on a single record as nodes - which seems the easiest option for getting out data from a single pull. Use detach nodes and process in pilot.
    addRecord = context.makeNewNode()
    addRecord.setName('record')
    for k, v in jiracols.items():
        # props.defineStringArrayProperty(k,v)
        addRecord.getProperties().defineStringProperty(k, v)
        root = data.getRoot()
        root.appendChild(addRecord)

    return pilotpython.READY_FOR_INPUT_OR_NEW_DATA
Пример #10
0
    def get_schema(cls, jira: JIRA) -> List[SchemaRow]:
        field_definitions: List[SchemaRow] = super().get_schema(jira)
        functions = get_installed_functions()

        for column in jira.fields():
            try:
                type = str(
                    cls.get_field_data(DotMap(column), "schema.type",
                                       functions))
            except NameNotDefined:
                type = ""
            field_definitions.append(
                SchemaRow.parse_obj({
                    "id":
                    str(cls.get_field_data(DotMap(column), "id", functions)),
                    "type":
                    type,
                    "description":
                    str(cls.get_field_data(DotMap(column), "name", functions)),
                    "raw":
                    DotMap(column),
                }))

        return field_definitions
Пример #11
0
def update_jira(release_note, cl, issues, no_issue):
    user = getpass.getuser()

    options = {'server': 'https://jira.company.com'}
    jira = JIRA(options, basic_auth=('myuser', 'mypass'))

    cl_field = ""
    release_note_field = ""
    for field in jira.fields():
        if field['name'] == 'Changeset':
            cl_field = field['id']
        elif field['name'] == 'Release Note':
            release_note_field = field['id']

        if cl_field != "" and release_note_field != "":
            break

    jira_issues = []
    jira_not_found = []

    for issue in issues:
        try:
            jira_issue = jira.issue(issue)
            jira_issues.append(jira_issue)
        except Exception as e:
            print(e)
            jira_not_found.append(issue)

    for jira_issue in jira_issues:

        orig_status = str(jira_issue.fields.status)

        # Skip JIRAs that are in one of the resolved states
        if (orig_status != "Fixed" and orig_status != "Accepted"
                and orig_status != "Closed" and orig_status != "Void"
                and orig_status != RESOLVED_STATUS):

            is_jira_ccr = (str(jira_issue.fields.issuetype) == ISSUE_TYPE_CCR)

            if is_jira_ccr:
                regression_test_name = jira_issue.raw['fields'][
                    regression_test_field]

                if (orig_status == REPORTER_REVIEW_STATUS):
                    transition_name = FIX_ACCEPTED_TRANSITION
                else:
                    transition_name = FIXED_STATUS_TRANSITION

            else:
                # This is a JIRA story
                if (orig_status == REPORTER_REVIEW_STATUS):
                    transition_name = FIX_ACCEPTED_TRANSITION
                else:
                    transition_name = ACCEPTED_STATUS

            try:
                jira.transition_issue(jira_issue, transition_name)

                # In case this is a JIRA CCR in State Reporter Review, 2 state transitions are needed to get to Fixed, so adding the second one
                # A false error is expected if the "Previous State" was "Fixed" but this is not a main flow scenario
                if (is_jira_ccr and (orig_status == REPORTER_REVIEW_STATUS)):
                    transition_name = FIXED_STATUS_TRANSITION
                    jira.transition_issue(jira_issue, transition_name)

            except Exception as error:
                if is_jira_ccr:
                    print(
                        "Cannot transit JIRA CCR %s to status 'Fixed' from '%s' using the '%s' transition"
                        % (str(jira_issue), jira_issue.fields.status,
                           transition_name))
                else:
                    print(
                        "Cannot transit JIRA Story %s to status 'Accepted' from '%s' using the '%s' transition"
                        % (str(jira_issue), jira_issue.fields.status,
                           transition_name))

                print("Error: %s" % error)

            jira.add_comment(jira_issue, comment)
            release_note = release_note + "\n" + str(
                jira_issue.raw['fields'][release_note_field])
            jira_issue.update(fields={
                cl_field: cl,
                release_note_field: release_note
            })
            print("\n\nINFO: ")

        else:
            print("No transaction is required for issue: " + str(jira_issue) +
                  " JIRA status is: " + str(jira_issue.fields.status))

    return ReturnValues.SUCCESS
Пример #12
0
class JiraClient(object):
    """
    Helper class for the JIRA
    """

    def __init__(self, url, username, password):
        """
        :param url:
        :param username:
        :param password:
        :return:
        """
        self.url = url
        self.webhook_url = self.url.strip('/') + '/rest/webhooks/1.0/webhook'
        self.basic_auth = (username, password)
        self.client = JIRA(url, basic_auth=self.basic_auth)

    def __str__(self):
        return '{} {}'.format(self.__class__.__name__, self.url)

################
#   Accounts   #
################

    def groups(self):
        """
        :return:
        """
        return self.client.groups()

    def users(self, json_result=True):
        """
        :param json_result:
        :return:
        """
        return self._search_users('_', json_result=json_result)

    def users_by_email(self, email, json_result=True):
        """
        :param email:
        :param json_result:
        :return:
        """
        return self._search_users(email, json_result=json_result)

    def users_by_group(self, name):
        """
        :param name:
        :return:
        """
        try:
            return self.client.group_members(name)
        except JIRAError as exc:
            logger.warning(exc)
            return dict()

    def _search_users(self, qstr, json_result=True):
        """
        :param qstr:
        :param json_result:
        :return:
        """
        def _user_dict_format(user):  # Note: Keep the consistent return format with the users_by_group method
            return {'key':      user.key,
                    'active':   user.active,
                    'fullname': user.displayName,
                    'email':    user.emailAddress
                    }

        users = self.client.search_users(qstr)

        if json_result:  # Note: Keep the consistent return format with the users_by_group method
            return [_user_dict_format(user) for user in users]
        else:
            return users

#############
#  Project  #
#############

    def create_project(self, key):
        """
        :param key:
        :return:
        """
        return self.client.create_project(key)

    def get_project(self, key, json_result=True):
        """
        :param key:
        :param json_result:
        :return:
        """
        if json_result:
            return self.client.project(key).raw
        else:
            return self.client.project(key)

    def get_projects(self, json_result=True):
        """
        :param json_result:
        :return:
        """
        project_objs = self.client.projects()
        if json_result:
            return [_each.raw for _each in project_objs]
        else:
            return project_objs

    def delete_project(self, key):
        """
        :param key:
        :return:
        """
        return self.client.delete_project(key)

#############
#  Version  #
#############

    def get_project_versions(self, name, json_result=True):
        """
        :param name: project name
        :param json_result:
        :return:
        """
        try:
            version_objs = self.client.project_versions(name)
            if json_result:
                return [_each.name for _each in version_objs]
            else:
                return version_objs
        except Exception as exc:
            logger.warn(exc)
            return []

    def create_project_version(self, name, project_name, **kwargs):
        """
        :param name: version name
        :param project_name: project name
        :param kwargs:
        :return:
        """
        return self.client.create_version(name, project_name, **kwargs)

#############
#   fields  #
########### #

    def get_fields(self):
        """
        :return:
        """
        return self.client.fields()

    def get_non_custom_fields(self):
        """
        :return:
        """
        return [each for each in self.client.fields() if not each.get('custom', True)]

    def get_custom_fields(self):
        """
        :return:
        """
        return [each for each in self.client.fields() if each.get('custom', True)]

    def get_field_id_by_name(self, name):
        """
        :param name:
        :return:
        """
        ids = [each['id'] for each in self.client.fields() if each.get('name', '') == name]
        if ids:
            return ids[0]
        else:
            return None

    def get_field_id_for_hours_left(self):
        """
        :return:
        """
        # For Argo customized field
        name = 'Hrs Left'
        return self.get_field_id_by_name(name)

############
#  issues  #
############

    def get_issue(self, name, json_result=True):
        """
        :param name:
        :param json_result:
        :return:
        """
        try:
            issue_obj = self.client.issue(name)
        except JIRAError as exc:
            logger.warn('Not found: %s', exc)
            return None

        if json_result:
            issue_dict = copy.deepcopy(issue_obj.raw['fields'])
            issue_dict['url'] = issue_obj.self
            issue_dict['id'] = issue_obj.id
            issue_dict['key'] = issue_obj.key
            # Replace custom field name
            return issue_dict
        else:
            return issue_obj

    def add_fix_version_to_issue(self, issue_name, version_name, issuetype=None):
        """
        :param issue_name:
        :param version_name:
        :param issuetype:
        :return:
        """
        return self._add_versions_from_issue(issue_name, version_name, issuetype=issuetype, _version_type='fixVersions')

    def add_affected_version_to_issue(self, issue_name, version_name, issuetype=None):
        """
        :param issue_name:
        :param version_name:
        :param issuetype:
        :return:
        """
        return self._add_versions_from_issue(issue_name, version_name, issuetype=issuetype, _version_type='versions')

    def _add_versions_from_issue(self, issue_name, version_name, issuetype=None, _version_type='fixVersions'):
        """
        :param issue_name:
        :param version_name:
        :param issuetype:
        :param _version_type:
        :return:
        """
        assert _version_type in ['fixVersions', 'versions'], 'Unsupported version type'

        issue_obj = self.get_issue(issue_name, json_result=False)

        if not issue_obj:
            return None

        if issuetype is not None and issue_obj.fields.issuetype.name.lower() != issuetype.lower():
            logger.info('SKIP. The issue type is %s, expected issue type is %s',
                        issue_obj.fields.issuetype.name.lower(),
                        issuetype.lower())
            return None

        logger.info('Update issue %s, with %s %s', issue_obj.key, _version_type, version_name)
        ret = issue_obj.add_field_value(_version_type, {'name': version_name})
        issue_obj.update()
        return ret

    def remove_affected_versions_from_issue(self, issue_name, versions_to_remove):
        """
        :param issue_name:
        :param versions_to_remove:
        :return:
        """
        return self._remove_versions_from_issue(issue_name, versions_to_remove, _version_type='versions')

    def remove_fix_versions_from_issue(self, issue_name, versions_to_remove):
        """
        :param issue_name:
        :param versions_to_remove:
        :return:
        """
        return self._remove_versions_from_issue(issue_name, versions_to_remove, _version_type='fixVersions')

    def _remove_versions_from_issue(self, issue_name, versions_to_remove, _version_type='fixVersions'):
        """
        :param issue_name:
        :param versions_to_remove:
        :param _version_type:
        :return:
        """
        assert _version_type in ['fixVersions', 'versions'], 'Unsupported version type'
        if type(versions_to_remove) not in [list, tuple, set]:
            versions_to_remove = [versions_to_remove]
        versions = []
        issue_obj = self.get_issue(issue_name, json_result=False)
        for ver in getattr(issue_obj.fields, _version_type):
            if ver.name not in versions_to_remove:
                versions.append({'name': ver.name})
        issue_obj.update(fields={_version_type: versions})

    def create_issue(self, project, summary, description=None, issuetype='Bug', reporter=None, **kwargs):
        """
        :param project:
        :param summary:
        :param description:
        :param issuetype:
        :param reporter:
        :param kwargs:
        :return:
        """
        # {
        #     "fields": {
        #        "project":
        #        {
        #           "key": "TEST"
        #        },
        #        "summary": "Always do right. This will gratify some people and astonish the REST.",
        #        "description": "Creating an issue while setting custom field values",
        #        "issuetype": {
        #           "name": "Bug"
        #        },
        #        "customfield_11050" : {"Value that we're putting into a Free Text Field."}
        #    }
        # }

        fields_dict = dict()

        for k, v in kwargs.items():
            fields_dict[k] = v

        fields_dict['project'] = {'key': project}
        fields_dict['description'] = description or ''
        fields_dict['summary'] = summary
        fields_dict['issuetype'] = {'name': issuetype}

        if reporter:
            users = self.users_by_email(reporter, json_result=False)
            if users:
                fields_dict['reporter'] = {'name': users[0].name}

        return self.client.create_issue(fields=fields_dict)

    def update_issue(self, name, **kwargs):
        """
        :param name:
        :param kwargs:
        :return:
        """
        issue_obj = self.get_issue(name, json_result=False)
        issue_obj.update(**kwargs)

######################
#   issue comments   #
######################

    def get_issue_comments(self, issue_name, latest_num=5, json_result=True):
        """
        :param issue_name:
        :param latest_num:
        :param json_result:
        :return:
        """
        try:
            comments = self.client.comments(issue_name)
        except JIRAError as exc:
            logger.warn(exc)
            return []
        comments = comments[::-1][:latest_num]
        if json_result:
            return [each.raw for each in comments]
        else:
            return comments

    def add_issue_comment(self, issue_name, msg, commenter=None):
        """
        :param issue_name:
        :param msg:
        :param commenter:
        :return:
        """
        if commenter:
            users = self.users_by_email(commenter, json_result=False)
            if users:
                msg_header = 'The comment is created by {}({}) from AX system. \n\n'.\
                    format(users[0].displayName, users[0].emailAddress)
                msg = msg_header + msg
        return self.client.add_comment(issue_name, msg)

###############
#  issue type #
###############

    def get_issue_types(self, json_result=True):
        """
        :param json_result:
        :return:
        """
        objs = self.client.issue_types()
        if json_result:
            return [obj.raw for obj in objs]
        else:
            return objs

    def get_issue_type_by_name(self, name, json_result=True):
        """
        :param name:
        :param json_result:
        :return:
        """
        try:
            obj = self.client.issue_type_by_name(name)
        except KeyError as exc:
            logger.warn(exc)
            return None
        else:
            if json_result:
                return obj.raw
            else:
                return obj

#############
#   Query   #
#############

    def query_issues(self, **kwargs):
        """
        :param kwargs:
        :return:

        max_results: maximum number of issues to return. Total number of results
                     If max_results evaluates as False, it will try to get all issues in batches.
        json_result: JSON response will be returned when this parameter is set to True.
                     Otherwise, ResultList will be returned
        """
        SUPPORTED_KEYS = ('project', 'status', 'component', 'labels', 'issuetype', 'priority',
                          'creator', 'assignee', 'reporter', 'fixversion', 'affectedversion')
        max_results = kwargs.pop('max_results', 100)
        _json_result = kwargs.pop('json_result', False)
        jql_str_list = []
        for k, v in kwargs.items():
            if k not in SUPPORTED_KEYS:
                continue
            jql_str_list.append('{} = "{}"'.format(k.strip(), v.strip()))
        if jql_str_list:
            jql_str = ' AND '.join(jql_str_list)
        else:
            jql_str = ''  # Fetch ALL issues
        try:
            ret = self.client.search_issues(jql_str, maxResults=max_results, json_result=_json_result)
        except Exception as exc:
            logger.warn(exc)
            ret = {"issues": []}
        return ret

################
# Query Issues #
################

    def get_issues_by_project(self, project_name, **kwargs):
        """
        :param project_name:
        :param kwargs:
        :return:
        """
        return self.query_issues(project=project_name, **kwargs)

    def get_issues_by_component(self, component, **kwargs):
        """
        :param component:
        :param kwargs:
        :return:
        """
        return self.query_issues(component=component, **kwargs)

    def get_issues_by_assignee(self, assignee, **kwargs):
        """
        :param assignee:
        :param kwargs:
        :return:
        """
        return self.query_issues(assignee=assignee, **kwargs)

    def get_issues_by_status(self, status, **kwargs):
        """
        :param status:
        :param kwargs:
        :return:
        """
        return self.query_issues(status=status, **kwargs)

    def get_issues_by_label(self, labels, **kwargs):
        """
        :param labels:
        :param kwargs:
        :return:
        """
        return self.query_issues(labels=labels, **kwargs)

    def get_issues_by_fixversion(self, fix_version, **kwargs):
        """
        :param fix_version:
        :param kwargs:
        :return:
        """
        return self.query_issues(fixverion=fix_version, **kwargs)

    def get_issues_by_affectedversion(self, affected_version, **kwargs):
        """
        :param affected_version:
        :param kwargs:
        :return:
        """
        return self.query_issues(affectedversion=affected_version, **kwargs)

################
#  Query Hours #
################

    def get_total_hours(self, **kwargs):
        """
        :param kwargs:
        :return:
        """
        all_issues = self.query_issues(**kwargs)
        field_id = self.get_field_id_for_hours_left()
        hours = [getattr(iss_obj.fields, field_id) for iss_obj in all_issues]
        return sum([float(each) for each in hours if each])

    def get_total_hours_by_project(self, project_name):
        """
        :param project_name:
        :return:
        """
        return self.get_total_hours(project=project_name)

    def get_total_hours_by_component(self, component):
        """
        :param component:
        :return:
        """
        return self.get_total_hours(component=component)

    def get_total_hours_by_assignee(self, assignee):
        """
        :param assignee:
        :return:
        """
        return self.get_total_hours(assignee=assignee)

    def get_total_hours_by_label(self, label):
        """
        :param label:
        :return:
        """
        return self.get_total_hours(labels=label)

##############
#   webhook  #
##############

    def create_ax_webhook(self, url, projects=None):
        """Create AX Jira webhook
        :param url:
        :param projects:
        :return:
        """
        payload = copy.deepcopy(AX_JIRA_WEBHOOK_PAYLOAD)
        payload['name'] = payload['name'] + self._get_cluster_name()
        payload['url'] = url
        filter_dict = self._generate_project_filter(projects)
        logger.info('Webhook project filter is: %s', filter_dict)
        payload.update(filter_dict)
        return self._requests(self.webhook_url, 'post', data=payload)

    def get_ax_webhook(self):
        """Get AX Jira webhook
        :return:
        """
        response = self._requests(self.webhook_url, 'get')
        wh_name = AX_JIRA_WEBHOOK_PAYLOAD['name'] + self._get_cluster_name()
        ax_whs = [wh for wh in response.json() if wh['name'] == wh_name]
        if not ax_whs:
            logger.error('Could not get Jira webhook for this cluster: %s, ignore it', wh_name)
        else:
            return ax_whs[0]

    def update_ax_webhook(self, projects=None):
        """Update AX Jira webhook
        :param projects:
        :return:
        """
        ax_wh = self.get_ax_webhook()
        if ax_wh:
            filter_dict = self._generate_project_filter(projects)
            logger.info('Webhook project filter is: %s', filter_dict)
            logger.info('Update the webhook %s', ax_wh['self'])
            return self._requests(ax_wh['self'], 'put', data=filter_dict)
        else:
            logger.warn('Skip the webhook update')

    def delete_ax_webhook(self):
        """Delete AX Jira webhook
        :return: 
        """
        response = self._requests(self.webhook_url, 'get')
        wh_name = AX_JIRA_WEBHOOK_PAYLOAD['name'] + self._get_cluster_name()

        ax_whs = [wh for wh in response.json() if wh['name'] == wh_name]
        for wh in ax_whs:
            logger.info('Delete webhook %s', wh['self'])
            self._delete_webhook(url=wh['self'])

    def get_ax_webhooks(self):
         """Get all AX webhooks
         :return:
         """
         response = self._requests(self.webhook_url, 'get')
         webhooks = response.json()
         # filter out non-ax webhooks
         return [wh for wh in webhooks if wh['name'].startswith(AX_JIRA_WEBHOOK_PAYLOAD['name'])]

    def delete_ax_webhooks(self):
        """Delete all AX Jira webhooks
        :return:
        """
        ax_whs = self.get_ax_webhooks()
        for wh in ax_whs:
            logger.info('Delete webhook %s', wh['self'])
            self._delete_webhook(url=wh['self'])

    def _generate_project_filter(self, projects):
        """
        :param projects:
        :return:
        """
        if not projects:
            filter_str = ''
        else:
            project_filter_list = []
            project_objs = self.get_projects(json_result=False)
            for pkey in projects:
                ps = [p for p in project_objs if p.key == pkey]
                if not ps:
                    logger.error('Could not get project %s, ignore it', pkey)
                else:
                    project_filter_list.append('Project = {}'.format(ps[0].name))
            filter_str = ' AND '.join(project_filter_list)

        return {'filters':
                    {'issue-related-events-section': filter_str
                     }
                }

    def _delete_webhook(self, url=None, id=None):
        """Delete webhook
        :param url:
        :param id:
        :return:
        """
        if url is None:
            url = self.webhook_url + '/' + str(id)
        return self._requests(url, 'delete')

    def _get_cluster_name(self):
        """
        :return:
        """
        return os.environ.get('AX_CLUSTER', UNKNOWN_CLUSTER)

    def _requests(self, url, method, data=None, headers=None, auth=None, raise_exception=True, timeout=30):
        """
        :param url:
        :param method:
        :param data:
        :param headers:
        :param auth:
        :param raise_exception:
        :param timeout:
        :return:
        """
        headers = {'Content-Type': 'application/json'} if headers is None else headers
        auth = self.basic_auth if auth is None else auth
        try:
            response = requests.request(method, url, data=json.dumps(data), headers=headers, auth=auth, timeout=timeout)
        except requests.exceptions.RequestException as exc:
            logger.error('Unexpected exception occurred during request: %s', exc)
            raise
        logger.debug('Response status: %s (%s %s)', response.status_code, response.request.method, response.url)
        # Raise exception if status code indicates a failure
        if response.status_code >= 400:
            logger.error('Request failed (status: %s, reason: %s)', response.status_code, response.text)
        if raise_exception:
            response.raise_for_status()
        return response
Пример #13
0
class JiraIntegrator:
    def __init__(self, user, api_token, host):
        options = {"server": host}
        self.jira_connection = JIRA(options, basic_auth=(user, api_token))
        self.event_handlers = {
            enums.GitHubAction.OPENED.value: self.create_new_jira_issue,
            enums.GitHubAction.CREATED.value: self.create_new_jira_issue,
            enums.GitHubAction.EDITED.value: self.update_jira_issue,
            enums.GitHubAction.LABELED.value: self.update_jira_label,
            enums.GitHubAction.UNLABELED.value: self.update_jira_label,
            enums.GitHubAction.CLOSED.value: self.close_jira_issue,
            enums.GitHubAction.REOPENED.value: self.reopen_jira_issue,
        }

    def handle(self, event):
        status_code = 500
        try:
            handler = self.event_handlers[event["action"]]
            handler(event["issue"], event["repository"])
            status_code = 200
        except (KeyError, ValueError) as err:
            status_code = 404
            logging.exception(f"Can not find: {repr(err)}")
        except Exception as err:
            logging.exception(f"Error occurs: {repr(err)}")

        return {
            "statusCode": status_code,
            "headers": {"Access-Control-Allow-Origin": "*"},
        }

    def create_new_jira_issue(self, issue, repository):
        issuetype = {"name": utils.get_issue_type(issue["labels"])}
        github_link_field_name = self._get_field_id("GitHub Link")
        github_author_field_name = self._get_field_id("GitHub Author")
        github_link = utils.find_github_link(repository["full_name"], issue["number"])
        labels = utils.get_jira_labels(issue["labels"])
        project = utils.find_project(repository)
        fields = {
            "project": project,
            "summary": issue["title"],
            "description": issue["body"],
            "issuetype": issuetype,
            "labels": labels,
            github_link_field_name: github_link,
            github_author_field_name: issue.get("user", {}).get("login"),
        }

        component = None

        try:
            if "mirumee/saleor-dashboard" in github_link:
                component = self.jira_connection.component(COMPONENT_ID_DASHBOARD)
            if "mirumee/saleor-docs" in github_link:
                component = self.jira_connection.component(COMPONENT_ID_DOCS)
        except Exception as err:
            logging.exception(f"Failed to assign component: {repr(err)}")

        if component:
            fields["components"] = [{"name": component.name}]

        self.jira_connection.create_issue(**fields)

    def update_jira_issue(self, issue, repository):
        jira_issue = self._find_jira_issue(repository["full_name"], issue["number"])
        fields = {"summary": issue["title"], "description": issue["body"]}

        jira_issue.update(fields=fields)

    def update_jira_label(self, issue, repository):
        jira_issue = self._find_jira_issue(repository["full_name"], issue["number"])
        issuetype = {"name": utils.get_issue_type(issue["labels"])}
        labels = utils.get_jira_labels(issue["labels"])
        fields = {"issuetype": issuetype, "labels": labels}

        jira_issue.update(fields=fields)

    def close_jira_issue(self, issue, repository):
        jira_issue = self._find_jira_issue(repository["full_name"], issue["number"])
        ready_to_test_transition = self._get_ready_to_test_transition(jira_issue)

        self.jira_connection.transition_issue(
            jira_issue, ready_to_test_transition["id"]
        )

    def reopen_jira_issue(self, issue, repository):
        jira_issue = self._find_jira_issue(repository["full_name"], issue["number"])
        to_do_transition = self._get_to_do_transition(jira_issue)

        self.jira_connection.transition_issue(jira_issue, to_do_transition["id"])

    def _get_field_id(self, field_name):
        fields = self.jira_connection.fields()
        selected_field = filter(
            lambda field: field["name"].lower() == field_name.lower(), fields
        )
        return next(selected_field)["key"]

    def _find_jira_issue(self, github_project, issue_number):
        query = '"GitHub Link" = "{github_link}"'.format(
            github_link=utils.find_github_link(github_project, issue_number)
        )
        issues = self.jira_connection.search_issues(query)
        if not issues:
            raise ValueError(f"Can not find jira issue with number {issue_number}")

        return issues[0]

    def _get_to_do_transition(self, issue):
        return self._get_transition_by_name(issue, enums.JiraTransition.TO_DO.value)

    def _get_ready_to_test_transition(self, issue):
        return self._get_transition_by_name(
            issue, enums.JiraTransition.READY_TO_TEST.value
        )

    def _get_transition_by_name(self, issue, transition):
        transitions = self.jira_connection.transitions(issue)
        ready_to_test_transition = next(
            filter(lambda tr: tr["name"].lower() == transition, transitions)
        )
        return ready_to_test_transition
Пример #14
0
if (not args.username):
    username = getpass.getuser()
    password = getpass.getpass('Enter your Jira password: '******'https://issues.voltdb.com/'

try:
    jira = JIRA(jira_url,
                basic_auth=(username, password),
                options=dict(verify=False))
except:
    sys.exit('FATAL: Unable to log in ' + username)

#Get Release Note field id
relnote_field = [
    f['id'] for f in jira.fields() if 'Release Note' in f['name']
][0]


def is_valid_jid(jid):
    #Return true of it looks like a valid ticket
    #return jid.split('-')[1].isdigit() and len(jid.split('-')) == 2
    if not len(jid.split('-')) == 2: return False
    return jid.split('-')[1].isdigit()


def cleanstring(str):
    return ' '.join(str.strip().split())


# Get the release notes from relnotesparser as a two column array
Пример #15
0
class JiraSession(object):
    def __init__(self, server, account, password, verify=True):
        """
        Init Jira Session
        :param server:
        :param account:
        :param password:
        :param verify:
        """
        self.__server = server
        self.__account = account
        self.__password = password
        self.__jira_opts = {
            'server': self.__server,
            'verify': verify,
        }
        self.__session = JIRA(self.__jira_opts,
                              basic_auth=(self.__account, self.__password))

    def __enter__(self):
        assert self.__session.current_user() == self.__account
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.__session.close()

    def get_user(self):
        """
        Get jira user
        :return:
        """
        logging.info(u'Get JIRA Current User')
        return self.__session.current_user()

    def search_issues(self, jql):
        """
        Search issues via JQL
        :param jql:
        :return:
        """
        logging.info(u'JIRA Search: %s' % jql)
        return self.__session.search_issues(jql_str=jql,
                                            maxResults=128,
                                            json_result=True)

    # def get_issue_count(self, jql: str, issue_summary: dict, issue_key: str):
    #     """
    #     Search issues via JQL and return count
    #     :param jql:
    #     :param issue_summary:
    #     :param issue_key:
    #     :return:
    #     """
    #     logging.info(u'JIRA Issue Count: %s' % jql)
    #     issue_summary[issue_key] = int(self.search_issues(jql).get('total'))
    #     return True

    def get_projects(self):
        """
        Get jira projects
        :return: <key, name, id>
        """
        logging.info(u'Get JIRA Projects')
        return self.__session.projects()

    def get_sprints(self):
        """
        Get jira sprints
        :return: <name, id>
        """
        logging.info(u'Get JIRA Sprints')
        jira_sprints = list()
        for board in self.__session.boards():
            _sprints = self.__session.sprints(board.id)
            jira_sprints = jira_sprints + _sprints
        return jira_sprints

    def get_issue_fields(self):
        """
        Get jira fields
        :return: [{'name':'','id':''}]
        """
        logging.info(u'Get JIRA Fields')
        _fields = list()
        for _field in self.__session.fields():
            _fields.append({'name': _field['name'], 'id': _field['id']})
        return _fields

    def get_issue_types(self):
        """
        Get jira issue types
        :return: <name, id>
        """
        logging.info(u'Get JIRA Issue Types')
        return self.__session.issue_types()

    def get_issue_statuses(self):
        """
        Get issue statuses
        :return: <name, id>
        """
        logging.info(u'Get JIRA Issue Statuses')
        return self.__session.statuses()

    def get_project_versions(self, pid: str):
        """
        Get project versions
        :param pid:
        :return: [<name, id>]
        """
        logging.info(u'Get JIRA Project %s Versions' % pid)
        return self.__session.project_versions(project=pid)
Пример #16
0
    file.close()
else:
    print("No file config.txt")
    file = config.open("w")
    file.write("JSESSIONID=\n")
    file.write("BASIC_LOGIN=\n")
    file.write("BASIC_PASS=\n")
    file.write("URL=")
    file.close()
    print("Created file config.txt. Fill it up with credentials!")
    exit()

options = {"cookies": {"JSESSIONID": sessionid}}

jira = JIRA(url, basic_auth=(basic_login, basic_pass), options=options)
nameMap = {field['name']: field['id'] for field in jira.fields()}

print("Getting issues...")
issues = searchIssues(
    "status = Done AND 'Story Points' is not EMPTY AND project != 'Zakupy Fenige' AND project != Urlopy AND resolved > startOfMonth(-6) ORDER BY resolved DESC",
    "Story Points,project,resolutiondate,customfield_10004")
print("Getting issues... DONE")

data = {
    "Key": [],
    "Project": [],
    "Story Points": [],
    "Team Name": [],
    "Resolve Date": []
}
def main():

    # Parse arguments. Assume any error handling happens in parse_args()
    try:
        args = parse_args()
    except Exception as err:
        print(f"Failed to parse arguments: {err}", file=sys.stderr)

    start_datetime = args["start_datetime"]
    end_datetime = args["end_datetime"]
    detailed = args["detailed"]

    num_issues_done = 0
    num_done_issues_code_reviewed = 0

    # Connect to Jira
    options = {"server": "https://opensciencegrid.atlassian.net"}
    jira = JIRA(options)

    # We need to find the key of the "Development" field, to determine if an issue has commits
    development_field_key = None
    fields = jira.fields()
    name_map = {field['name']: field['id'] for field in fields}
    if "Development" in name_map:
        development_field_key = name_map["Development"]

    # Iterate over all completed issues in the HTCONDOR project
    issues = jira.search_issues(
        "project = HTCONDOR AND type in (Improvement, Bug) AND status = Done",
        expand="changelog",
        maxResults=False)
    for issue in issues:
        issue_changelog = issue.changelog.histories
        issue_isdone = False
        for change in issue_changelog:
            for item in change.items:
                # Issues can be marked Done multiple times, so break after first occurrence
                if issue_isdone:
                    break
                # Check if this change was setting the status to Done
                if item.field == "status" and item.toString == "Done":
                    # Check if this issue has any code commits. If not, don't count it.
                    development_data = getattr(issue.fields,
                                               development_field_key)
                    if development_data == "{}":
                        continue
                    # Strip the time offset from the change.created string
                    changed = change.created[0:change.created.rfind("-")]
                    # Was this issue set to Done status between the provided end and start dates?
                    changed_datetime = datetime.strptime(
                        changed, "%Y-%m-%dT%H:%M:%S.%f")
                    if changed_datetime > start_datetime and changed_datetime < end_datetime:
                        if detailed is True:
                            print(
                                f"{issue.key}: {issue.fields.summary}, Marked Done: {changed_datetime.strftime('%Y-%m-%d %H:%M:%S')}"
                            )
                        num_issues_done += 1
                        issue_isdone = True
                        # Now check the issue comments for a "code review" text entry
                        comments = jira.comments(issue)
                        if len(comments) > 0:
                            for comment in comments:
                                if "code review" in comment.body.lower()[0:20]:
                                    num_done_issues_code_reviewed += 1
                                    if detailed is True:
                                        print("\tThis issue was code reviewed")
                                    break

    print(
        f"\nBetween {start_datetime.strftime('%Y-%m-%d')} and {end_datetime.strftime('%Y-%m-%d')}:\n"
    )
    print(f"{num_issues_done} HTCONDOR issues were marked Done")
    print(
        f"{num_done_issues_code_reviewed} of these completed issues were code reviewed"
    )
    if num_issues_done > 0:
        print(
            f"Code review rate: {round(num_done_issues_code_reviewed*100/num_issues_done, 2)}%\n"
        )
    else:
        print(f"No issues marked Done between the dates specified.")
Пример #18
0
class Jira:

    # {{{ Constants
    BLOCKS = 'Blocks'
    DUPLICATE = 'Duplicate'
    RELATES = 'Relates'

    # }}}

    # {{{ init(address) - Initialise JIRA class, pointing it to the JIRA endpoint
    def __init__(self, address='https://r3-cev.atlassian.net'):
        self.address = address
        self.jira = None
        self.mock_key = 1
        self.custom_fields_by_name, self.custom_fields_by_key = {}, {}

    # }}}

    # {{{ login(user, password) - Log in as a specific JIRA user
    def login(self, user, password):
        try:
            self.jira = JIRA(self.address, auth=(user, password))
            for x in self.jira.fields():
                if x['custom']:
                    self.custom_fields_by_name[x['name']] = x['key']
                    self.custom_fields_by_key[x['key']] = x['name']
            return self
        except Exception as error:
            message = error.message
            if isinstance(error, JIRAError):
                message = error.text if error.text and len(
                    error.text) > 0 and not error.text.startswith(
                        '<!') else message
            raise Exception('failed to log in to JIRA{}{}'.format(
                ': ' if message else '', message))

    # }}}

    # {{{ search(query) - Search for issues and manually traverse pages if multiple pages are returned
    def search(self, query, *args):
        max_count = 50
        index, offset, count = 0, 0, max_count
        query = query.format(*args) if len(args) > 0 else query
        while count == max_count:
            try:
                issues = self.jira.search_issues(query,
                                                 maxResults=max_count,
                                                 startAt=offset)
                count = len(issues)
                offset += count
                for issue in issues:
                    index += 1
                    yield Issue(self, index=index, issue=issue)
            except JIRAError as error:
                raise Exception('failed to run query "{}": {}'.format(
                    query, error.text))

    # }}}

    # {{{ find(key) - Look up issue by key
    def find(self, key):
        try:
            issue = self.jira.issue(key)
            return Issue(self, issue=issue)
        except JIRAError as error:
            raise Exception('failed to look up issue "{}": {}'.format(
                key, error.text))

    # }}}

    # {{{ create(fields, dry_run) - Create a new issue
    def create(self, fields, dry_run=False):
        if dry_run:
            return Issue(self, fields=fields)
        try:
            fields['labels'] = filter(lambda x: x is not None,
                                      fields['labels'])
            issue = self.jira.create_issue(fields)
            return Issue(self, issue=issue)
        except JIRAError as error:
            raise Exception('failed to create issue: {}'.format(error.text))

    # }}}

    # {{{ link(issue_key, other_issue_key, relationship, dry_run) - Link one issue to another
    def link(self,
             issue_key,
             other_issue_key,
             relationship=RELATES,
             dry_run=False):
        if dry_run:
            return
        try:
            self.jira.create_issue_link(type=relationship,
                                        inwardIssue=issue_key,
                                        outwardIssue=other_issue_key,
                                        comment={
                                            'body':
                                            'Linked {} to {}'.format(
                                                issue_key, other_issue_key),
                                        })
        except JIRAError as error:
            raise Exception('failed to link {} and {}: {}'.format(
                issue_key, other_issue_key, error.text))
class JiraAnalysis:
    def __init__(self, jira_url, jira_username, jira_token, jira_team_labels):
        self.issue_cache = {}
        self.jira = JIRA(jira_url, basic_auth=(jira_username, jira_token))
        self.jira_team_labels = jira_team_labels
        self.sprint_field = self.get_custom_field_key("Sprint")
        self.story_point_field = self.get_custom_field_key("Story Points")
        self.investment_area_field = self.get_custom_field_key(
            "Investment Area")
        self.epic_link_field = self.get_custom_field_key("Epic Link")

        if self.sprint_field is None:
            raise Exception("Failed to find Sprint field")

        if self.story_point_field is None:
            raise Exception("Failed to find Story Point field")

        if self.investment_area_field is None:
            raise Exception("Failed to find Investment Area field")

        if self.epic_link_field is None:
            raise Exception("Failed to find Epic Link field")

    # Retrieve the custom field matching to a particular name since JIRA gives custom fields a random ID
    def get_custom_field_key(self, name):
        all_fields = self.jira.fields()
        for field in all_fields:
            if field["name"] == name:
                return field["key"]
        return None

    # Retrieve the team for an issue - based on labels
    def get_team(self, issue):
        for label in issue.fields.labels:
            for team_label in self.jira_team_labels:
                if team_label.lower() == label.lower():
                    return team_label
        return None

    # Retrieve the investment area
    def get_investment_area(self, issue):
        ia = getattr(issue.fields, self.investment_area_field)
        if ia:
            return ia
        return []

    # Retrieve the epic link
    def get_epic_link(self, issue):
        return getattr(issue.fields, self.epic_link_field, "")

    # Get issue type, a bit weird since it's off of fields and needs to be converted to string
    def get_issue_type(self, issue):
        return issue.fields.issuetype.name.lower()

    # Get description from an issue
    def get_description(self, issue):
        return issue.fields.description

    # Get story points from an issue
    def get_story_points(self, issue):
        return get_or_float_zero(issue.fields, self.story_point_field)

    # Get the priority of an issue
    def get_priority(self, issue):
        for label in issue.fields.labels:
            # TODO: Update this for other formats
            # Take the first label found to avoid double counting
            if "2018:q1:" in label.lower() or "2018:q2" in label.lower():
                try:
                    return int(label.split(":")[-1])
                except:
                    return 100
        # The Misc priority
        return 100

    # For a set of issues get stats by priority
    def get_priority_stats(self, issues):
        priority_count = Counter()
        priority_story_points = defaultdict(float)
        no_priority_stories = []
        for issue in issues:
            priority = self.get_priority(issue)
            if priority is not None:
                story_points = self.get_story_points(issue)
                priority_count.update([priority])
                priority_story_points[priority] += float(story_points)
            else:
                no_priority_stories.append(issue)
        return priority_count, priority_story_points, no_priority_stories

    # Wrap the pagination code so user doesn't have to do it themselves
    def get_issues(self, query):
        if query in self.issue_cache:
            return self.issue_cache[query]

        all_issues = []
        MAX_RESULTS = 100
        issues = self.jira.search_issues(query, maxResults=MAX_RESULTS)
        total = issues.total
        all_issues.extend(list(issues))
        if total > MAX_RESULTS:  # Actually need to paginate
            for page in range(1, int(ceil(1.0 * total / MAX_RESULTS))):
                logger.info("Getting page %s", page)
                issues = self.jira.search_issues(query,
                                                 maxResults=MAX_RESULTS,
                                                 startAt=page * MAX_RESULTS)
                logger.info("Retrieved %s issues", len(issues))
                all_issues.extend(list(issues))
        logger.info("Total retrieved %s", len(all_issues))
        self.issue_cache[query] = all_issues
        return all_issues

    # Clean up and write issues to a CSV
    def write_issues(self, start_date, end_date, fn):
        issues = self.get_issues(self.get_issue_query(start_date, end_date))
        with open(fn, "w") as f:
            w = csv.writer(f)
            w.writerow([
                "ticket",
                "summary",
                "team",
                "priority",
                "story_points",
                "assignee",
                "created_date",
                "resolved_date",
                "type",
                "investment_area",
                "epic",
            ])
            for issue in issues:
                w.writerow([
                    issue,
                    issue.fields.summary.encode("utf-8"),
                    self.get_team(issue),
                    self.get_priority(issue),
                    self.get_story_points(issue),
                    issue.fields.assignee if issue.fields.assignee else "None",
                    issue.fields.created,
                    issue.fields.resolutiondate,
                    self.get_issue_type(issue),
                    ",".join(self.get_investment_area(issue)),
                    self.get_epic_link(issue),
                ])

    # Get all done stories and bugs between a date range
    def get_issue_query(self, start_date, end_date):
        return ('project = "TL" and status = Done and resolutiondate >= "' +
                start_date + '" and resolutiondate <= "' + end_date +
                '" AND type in ("story", "bug", "task", "spike", "access")')

    # Just get the list of words
    def get_descriptions_words(self, start_date, end_date):
        issues = self.get_issues(self.get_issue_query(start_date, end_date))
        words = []
        for issue in issues:
            desc = self.get_description(issue)
            if desc is not None:
                words.extend(desc.split(" "))
        return words

    # Measure analytics per priority
    def analyze_priorities(self, start_date, end_date):
        issues = self.get_issues(self.get_issue_query(start_date, end_date))
        (
            priority_count,
            priority_story_points,
            no_priority_stories,
        ) = self.get_priority_stats(issues)

        logger.info("Priority counts")
        logger.info(print_dict(priority_count, "\n"))

        logger.info("Priority story points")
        logger.info(print_dict(priority_story_points, "\n"))

        logger.info("No priorities")
        for issue in no_priority_stories:
            logger.info("\t %s %s", issue, issue.fields.summary)

    # Measrure # of sprints to do a story
    def analyze_sprint_lag(self, start_date, end_date):
        team_sprint_counts = defaultdict(list)
        team_sprint_story_point_sum = defaultdict(float)
        team_story_point_sum = defaultdict(float)
        team_bugs = defaultdict(int)
        issues = self.get_issues(self.get_issue_query(start_date, end_date))
        for issue in issues:
            team = self.get_team(issue)
            try:
                num_sprints = len(getattr(issue.fields, self.sprint_field))
            except:
                num_sprints = 0
            story_points = get_or_float_zero(issue.fields,
                                             self.story_point_field)
            issue_type = self.get_issue_type(issue)

            # Has a team and was actually done via sprint process
            if team and num_sprints > 0 and issue_type == "story":
                team_sprint_counts[team].append(num_sprints)

                if story_points > 0:
                    team_sprint_story_point_sum[
                        team] += num_sprints * story_points
                    team_story_point_sum[team] += story_points

            if issue_type == "bug":
                team_bugs[team] += 1

        logger.info("Team\tSprint Lag\tSP Sprint Lag\tBugs")
        for team, counts in team_sprint_counts.items():
            if team_sprint_story_point_sum[team] > 0:
                logger.info(
                    "%s\t%s\t%s\t%s",
                    team,
                    sum(counts) * 1.0 / len(counts),
                    team_sprint_story_point_sum[team] /
                    team_story_point_sum[team],
                    team_bugs[team],
                )
            else:
                logger.info("%s\tNA\t%NA\t%s", team, team_bugs[team])

    # Measure # of story points done per assignee
    def analyze_story_points(self, start_date, end_date):
        user_story_point_sum = Counter()
        user_data = {}
        issues = self.get_issues(self.get_issue_query(start_date, end_date))
        for issue in issues:
            story_points = get_or_float_zero(issue.fields,
                                             self.story_point_field)
            assignee = str(
                issue.fields.assignee) if issue.fields.assignee else "None"
            user_story_point_sum.update({assignee: int(story_points)})
            if assignee not in user_data:
                user_data[assignee] = defaultdict(int)
            issue_type = self.get_issue_type(issue)
            user_data[assignee][issue_type + "_cnt"] += 1
            user_data[assignee][issue_type +
                                "_story_points"] += int(story_points)

        logger.info("User\tSP\tStories\tBugs")
        for user, story_points in user_story_point_sum.most_common(100):
            logger.info("%s\t%s\t%s", user, story_points, user_data[user])
Пример #20
0
    def generate(self):
        storage_passwords = self.service.storage_passwords
        jira_username = None
        jira_password = None
        for credential in storage_passwords:
            credential_realm = credential.content.get('realm')
            if credential_realm == self.jirasearch_realm:
                jira_username = credential.content.get('username')
                jira_password = credential.content.get('clear_password')

        if not jira_username:
            raise Exception("Did not find username/password for realm: {}".format(self.jirasearch_realm))

        try:
            jira_url = self.service.confs['jirasearch']['jirasearch']['jira_url']
        except:
            raise Exception("Did not find jira_url setting in [jirasearch] in jirasearch.conf")
        
        try:
            jira = JIRA(jira_url, basic_auth=(jira_username, jira_password))
        except:
            raise Exception("Unable to connect to JIRA with configured URL/username/password")


        field_id_for_field = {}
        for field in jira.fields():
            field_id_for_field[field['name']] = field['id']

        # if we can't access search_et, set it to the epoch time
        try:
            search_et_epoch = self.search_results_info.search_et
        except:
            search_et_epoch = 0.0

        # if we can't access search_lt, set it to now
        try:
            search_lt_epoch = self.search_results_info.search_lt
        except:
            search_lt_epoch = time.time()

        # format timestamps to be appropriate for JQL
        search_et_jira = datetime.fromtimestamp(search_et_epoch).strftime("%Y/%m/%d %H:%M")
        search_lt_jira = datetime.fromtimestamp(search_lt_epoch).strftime("%Y/%m/%d %H:%M")

        # perform substitution
        templated_query = Template(self.query).render(earliest=search_et_jira, latest=search_lt_jira)

        events = []
        for issue in jira.search_issues(templated_query, maxResults=self.limit):
            event = {
                '_time': time.mktime(time.strptime(issue.fields.created, "%Y-%m-%dT%H:%M:%S.000+0000")),
                '_raw': "{}: {}".format(issue.key, issue.fields.summary),
                'key': issue.key,
            }
            for field in field_id_for_field:
                field_id = field_id_for_field[field]
                try:
                    field_value = getattr(issue.fields, field_id)
                    if isinstance(field_value, list):
                        event[field] = []
                        for value in field_value:
                            event[field].append("{}".format(value))
                    elif field_value:
                        event[field] = "{}".format(field_value)
                    else:
                        event[field] = []
                except:
                    event[field] = []

            events.append(event)

        sorted_events = sorted(events, None, lambda x: x['_time'], True)
        for event in sorted_events:
            yield event
Пример #21
0
class JiraIssues(object):
    APPLICATION = {"type": "www.hackerone.comr", "name": "Hacker One"}

    SCOPE = '''
h4.Scope
----
asset type: %(type)s
asset identifier: %(identifier)s\n'''

    DESCRIPTION = '''
h4.Report Info
----
Report State: %(state)s
Reporter: %(reporter)s
Assignee: %(assignee)s
Report Created: %(created)s
Report Last Activity: %(last_activity)s

h4.Weakness
----
name: %(name)s
description: %(w_description)s
id: %(id)s

h4.Severity
----
rating: %(rating)s
score: %(score)s'

h4.Description
----
%(description)s
'''

    def __init__(self, server, username, password, project):
        """Inits jira client.  This current setup requires a jira username to be setup with the appropriate
        permissions in the jira project

        :type server: string
        :param server: jira url

        :type username: string
        :param username: token

        :type password: string
        :param password: jira username password

        :type project: string
        :param project: jira project
        """
        self.__jira_server = server
        self.__username = username
        self.__password = password
        self.jira_project = project
        self._init_jira_client()

    def _init_jira_client(self):
        options = {'server': self.__jira_server}

        def create_custom_field(fields=None):
            url = self._get_url('field')
            r = self._session.post(url, data=json.dumps(fields))

            if r.status_code != 201:
                raise JIRAError(r.status_code, request=r)

            return r

        # Jira library doesn't have method for creating custom fields
        setattr(JIRA, 'create_custom_field', create_custom_field)

        self.jira_client = JIRA(options,
                                basic_auth=(self.__username, self.__password))

    def get_jira_projects(self):
        return self.jira_client.projects()

    def create_project(self, key, name, jira_type="Software"):
        return self.jira_client.create_project(key, name, jira_type)

    def get_jira_issue(self, report):
        """
        Return Jira Issue based on HackerOne Report issue_tracker_reference_id

        :type report: h1.models.Report
        :param report: hackerone report
        :return: Jira Issue
        """
        try:
            return self.jira_client.issue(report.issue_tracker_reference_id)
        except JIRAError as e:
            if e.text == "Issue Does Not Exist":
                return None
            else:
                raise

    @staticmethod
    def _get_jira_summary(report):
        return "%s - %s" % (report.id, report.title)

    def _get_jira_description(self, report):
        return self.DESCRIPTION % {
            'description': report.vulnerability_information,
            'reporter': report.reporter.name,
            'assignee':
            report.assignee.name if report.assignee is not None else "",
            'state': report.state,
            'created': report.created_at,
            'last_activity': report.last_activity_at,
            'name': report.weakness.name,
            'w_description': report.weakness.description,
            'id': report.weakness.external_id,
            'rating': report.severity.rating,
            'score': report.severity.score
        }

    def create_jira_issue(self, report):
        """
        Create Jira Issue
        https://developer.atlassian.com/server/jira/platform/jira-rest-api-example-create-issue-7897248/

        :type report: h1.models.Report
        :param report: hackerone report

        :type :return: string
        :return: Jira ID
        """
        issue_dict = {
            'project': {
                'key': self.jira_project
            },
            'summary': self._get_jira_summary(report),
            'description': self._get_jira_description(report),
            'issuetype': {
                'name': 'Bug'
            },
            'labels': ['hackerOne']
        }

        return self.jira_client.create_issue(fields=issue_dict, prefetch=True)

    def update_jira_issue(self, report, jira):
        fields = {}

        summary = self._get_jira_summary(report)

        if jira.fields.summary != summary:
            fields['summary'] = summary

        description = self._get_jira_description(report)

        if jira.fields.description != description:
            fields['description'] = description

        if fields:
            logging.info("Updating Existing Jira Issue: %s" % fields.keys())
            jira.update(fields=fields)

    def search_for_jira_issues(self, report_id):
        """
        Perform a Jira query search using JQL
        :param report_id: hacker one report id
        :return: returns jira issue match
        """
        return self.jira_client.search_issues(
            '''project = %s AND summary ~ "%s"''' %
            (self.jira_project, report_id),
            maxResults=1)

    def get_fields(self):
        return self.jira_client.fields()

    def create_custom_field(self, fields):
        return self.jira_client.create_custom_field(fields)

    def get_remote_links(self, jira):
        return self.jira_client.remote_links(jira)

    def add_remote_link(self, report, jira, relationship="Relates"):
        links = set()

        # note all rmeote links have to have a global id
        for link in self.get_remote_links(jira):
            if hasattr(link, 'globalId'):
                links.add(link.globalId)

        if report.id not in links:
            destination = {'url': report.html_url, 'title': report.title}
            return self.jira_client.add_remote_link(jira, destination,
                                                    report.id,
                                                    self.APPLICATION,
                                                    relationship)

    def add_simple_link(self, report, jira):
        """https://developer.atlassian.com/server/jira/platform/jira-rest-api-for-remote-issue-links/"""
        link = {'url': report.html_url, 'title': report.title}

        return self.jira_client.add_simple_link(jira, object=link)

    def add_jira_attachment(self, jira, attachment, filename):
        """Add H1 Attachment in Jira

        :param jira: Jira object that has attachments
        :param attachment: hacker one attachment object content
        :param filename: attachment file name
        :return: return
        """
        return self.jira_client.add_attachment(issue=jira.id,
                                               attachment=attachment,
                                               filename=filename)

    def create_comments(self, jira, comment):
        return self.jira_client.add_comment(jira, comment)
Пример #22
0
class AugurJira(object):
    """
    A thin wrapper around the Jira module providing some refinement for things like fields, convenience methods
    for ticket actions and awareness for Augur-specific data types.
    """
    jira = None

    def __init__(self, server=None, username=None, password=None):

        self.logger = logging.getLogger("augurjira")
        self.server = server or settings.main.integrations.jira.instance
        self.username = username or settings.main.integrations.jira.username
        self.password = password or settings.main.integrations.jira.password
        self.fields = None

        self.jira = JIRA(basic_auth=(self.username, self.password),
                         server=self.server,
                         options={"agile_rest_path": "agile"})

        self._field_map = {}

        self._default_fields = munchify({
            "summary": None,
            "description": None,
            "status": None,
            "priority": None,
            "parent": None,
            "resolution": None,
            "epic link": None,
            "dev team": None,
            "labels": None,
            "issuelinks": None,
            "development": None,
            "reporter": None,
            "assignee": None,
            "issuetype": None,
            "project": None,
            "creator": None,
            "attachment": None,
            "worklog": None,
            "story points": None,
            "changelog": None
        })

        self.fields = api.get_memory_cached_data('custom_fields')
        if not self.fields:
            fields = api.memory_cache_data(self.jira.fields(), 'custom_fields')
            self.fields = {f['name'].lower(): munchify(f) for f in fields}

        default_fields = {}
        for df, val in self._default_fields.items():
            default_fields[df] = self.get_field_by_name(df)

        self._default_fields = munchify(default_fields)

    @property
    def default_fields(self):
        """
        Returns a dict containing the friendly name of fields as keys and jira's proper field names as values.
        :return: dict
        """
        return self._default_fields

    def get_field_by_name(self, name):
        """
        Returns the true field name of a jira field based on its friendly name
        :param name: The friendly name of the field
        :return: A string with the true name of a field.
        """
        assert self.fields

        try:
            _name = name.lower()
            if _name.lower() in self.fields:
                return self.fields[_name]['id']

            else:
                return name

        except (KeyError, ValueError):
            return name

    def link_issues(self, link_type, inward, outward, comment=None):
        """
        Establishes a link in jira between two issues
        :param link_type: A string indicating the relationship from the inward to the outward
         (Example: "is part of this release")
        :param inward: Can be one of: Issue object, Issue dict, Issue key string
        :param outward: Can be one of: Issue object, Issue dict, Issue key string
        :param comment: None or a string with the comment associated with the link
        :return: No return value.
        """
        ""
        if isinstance(inward, dict):
            inward_key = inward['key']
        elif isinstance(inward, Issue):
            inward_key = inward.key
        elif isinstance(inward, str):
            inward_key = inward
        else:
            raise TypeError("'inward' parameter is not of a valid type")

        if isinstance(outward, dict):
            outward_key = outward['key']
        elif isinstance(outward, Issue):
            outward_key = outward.key
        elif isinstance(outward, str):
            outward_key = outward
        else:
            raise TypeError("'outward' parameter is not of a valid type")

        self.jira.create_issue_link(link_type, inward_key, outward_key,
                                    comment)

    def create_ticket(self, create_fields, update_fields=None, watchers=None):
        """
        Create the ticket with the required fields above.  The other keyword arguments can be used for other fields
           although the values must be in the correct format.
        :param update_fields:
        :param create_fields: All fields to include in the creation of the ticket. Keys include:
                project: A string with project key name (required)
                issuetype: A dictionary containing issuetype info (see Jira API docs) (required)
                summary: A string (required)
                description: A string
        :param update_fields: A dictionary containing reporter info  (see Jira API docs)
        :param watchers: A list of usernames that will be added to the watch list.

        :return: Return an Issue object or None if failed.
        """
        try:
            ticket = self.jira.create_issue(create_fields)

            if ticket:
                try:
                    # now update the remaining values (if any)
                    # we can't do this earlier because assignee and reporter can't be set during creation.
                    if update_fields and len(update_fields) > 0:
                        ticket.update(update_fields)
                except Exception as e:
                    self.logger.warning(
                        "Ticket was created but not updated due to exception: %s"
                        % e.message)

                try:
                    if watchers and isinstance(watchers, (list, tuple)):
                        [self.jira.add_watcher(ticket, w) for w in watchers]
                except Exception as e:
                    self.logger.warning(
                        "Unable to add watcher(s) due to exception: %s" %
                        e.message)

            return ticket

        except Exception as e:
            self.logger.error("Failed to create ticket: %s", e.message)
            return None
Пример #23
0
class JiraRealBackend(JiraBaseBackend):
    """ NodeConductor interface to JIRA """

    class Resource(object):
        """ Generic JIRA resource """

        def __init__(self, manager):
            self.manager = manager

    class Issue(Resource):
        """ JIRA issues resource """

        class IssueQuerySet(object):
            """ Issues queryset acceptable by django paginator """

            def filter(self, term):
                if term:
                    escaped_term = re.sub(r'([\^~*?\\:\(\)\[\]\{\}|!#&"+-])', r'\\\\\1', term)
                    self.query_string = self.base_query_string + ' AND text ~ "%s"' % escaped_term
                return self

            def _fetch_items(self, offset=0, limit=1, force=False):
                # Default limit is 1 because this extra query required
                # only to determine the total number of items
                if hasattr(self, 'items') and not force:
                    return self.items

                try:
                    self.items = self.query_func(
                        self.query_string,
                        fields=self.fields,
                        startAt=offset,
                        maxResults=limit)
                except JIRAError as e:
                    logger.exception(
                        'Failed to perform issues search with query "%s"', self.query_string)
                    six.reraise(JiraBackendError, e)

                return self.items

            def __init__(self, jira, query_string, fields=None):
                self.fields = fields
                self.query_func = jira.search_issues
                self.query_string = self.base_query_string = query_string

            def __len__(self):
                return self._fetch_items().total

            def __iter__(self):
                return self._fetch_items()

            def __getitem__(self, val):
                return self._fetch_items(offset=val.start, limit=val.stop - val.start, force=True)

        def create(self, summary, description='', reporter='', assignee=None):
            args = {
                'summary': summary,
                'description': description,
                'project': {'key': self.manager.core_project},
                'issuetype': {'name': self.manager.default_issue_type},
            }

            # Validate reporter & assignee before actual issue creation
            if assignee:
                assignee = self.manager.users.get(assignee)
            if self.manager.reporter_field:
                args[self.manager.reporter_field_id] = reporter
            elif reporter:
                reporter = self.manager.users.get(reporter)

            try:
                issue = self.manager.jira.create_issue(fields=args)

                if reporter and not self.manager.reporter_field:
                    issue.update(reporter={'name': reporter.name})
                if assignee:
                    self.manager.jira.assign_issue(issue, assignee.key)

            except JIRAError as e:
                logger.exception('Failed to create issue with summary "%s"', summary)
                six.reraise(JiraBackendError, e)

            return issue

        def get_by_user(self, username, user_key):
            try:
                issue = self.manager.jira.issue(user_key)
            except JIRAError:
                raise JiraBackendError("Can't find issue %s" % user_key)

            if self.manager.reporter_field:
                is_owner = getattr(issue.fields, self.manager.reporter_field_id) == username
            else:
                reporter = self.manager.users.get(username)
                is_owner = issue.fields.reporter.key == reporter.key

            if not is_owner:
                raise JiraBackendError("Access denied to issue %s for user %s" % (user_key, username))

            return issue

        def list_by_user(self, username):
            if self.manager.reporter_field:
                query_string = "project = {} AND '{}' ~ '{}'".format(
                    self.manager.core_project, self.manager.reporter_field, username)
            else:
                query_string = "project = {} AND reporter = {}".format(
                    self.manager.core_project, username)
            query_string += " order by updated desc"

            return self.IssueQuerySet(self.manager.jira, query_string)

    class Comment(Resource):
        """ JIRA issue comments resource """

        def list(self, issue_key):
            try:
                return self.manager.jira.comments(issue_key)
            except JIRAError as e:
                logger.exception(
                    'Failed to perform comments search for issue %s', issue_key)
                six.reraise(JiraBackendError, e)

        def create(self, issue_key, comment):
            return self.manager.jira.add_comment(issue_key, comment)

    class User(Resource):
        """ JIRA users resource """

        def get(self, username):
            try:
                return self.manager.jira.user(username)
            except JIRAError:
                raise JiraBackendError("Unknown JIRA user %s" % username)

    def __init__(self, settings, core_project=None, reporter_field=None, default_issue_type='Task'):
        self.settings = settings
        self.core_project = core_project
        self.reporter_field = reporter_field
        self.default_issue_type = default_issue_type

        if settings.dummy:
            self.jira = JiraDummyClient()
        else:
            self.jira = JIRA(
                {'server': settings.backend_url, 'verify': False},
                basic_auth=(settings.username, settings.password), validate=False)

        if self.reporter_field:
            try:
                self.reporter_field_id = next(
                    f['id'] for f in self.jira.fields() if self.reporter_field in f['clauseNames'])
            except StopIteration:
                raise JiraBackendError("Can't custom field %s" % self.reporter_field)

        self.users = self.User(self)
        self.issues = self.Issue(self)
        self.comments = self.Comment(self)
Пример #24
0
from jira import JIRA

# instantiate jira
jira = JIRA(server='server', basic_auth=('userid', 'Password'))

issues = jira.fields()
jiracols = {}
#print(issues)
for i in issues:
    #print(i['name']+"_"+i['id'])
    jiracols[i['name']] = i['id']

print(jiracols)
for k, v in jiracols.items():
    print(k)
    print(v)
    print("NEXT!!")
Пример #25
0
class JiraDataStructuresManager():
    ''' Extracts structural data from Jira for further analysis '''
    options = None
    jira = None
    cfg = None

    def __init__(self):
        # By default, the client will connect to a JIRA instance started from the Atlassian Plugin SDK.
        # See https://developer.atlassian.com/display/DOCS/Installing+the+Atlassian+Plugin+SDK
        # for details.
        self.cfg = PropertiesHandler()

        self.options = {'server': __JIRA_SERVER__}
        self.jira = JIRA(
            self.options,
            basic_auth=(self.cfg.user,
                        self.cfg.pwd))  # a username/password tuple

    def getAllSprintNames(self):
        '''get all boards names for internal use.'''
        boards = self.jira.boards()
        sprints = None
        csv = "Board Name; SprintName;State\n"

        for board in boards:

            try:
                sprints = self.jira.sprints(board.id)

                for s in sprints:
                    csv += board.name + ";" + s.name + ";" + s.state + "\n"

            except Exception:
                pass

        return csv

    def getSprintListNamesAndState(self, boardName):
        '''It returns the project's associated sprints and status for its usage in other reports. '''

        try:
            sprints = self.jira.sprints(boardName, False, 0, None, None)
        except Exception:  # TODO: put the right error handling.
            return "Error: Scrum Board hasn't sprints associated."

        csv = "SprintName;State\n"

        for s in sprints:
            csv += s.name + ";" + s.state + "\n"

        return csv

    def getAllProjectsAsCSV(self):
        '''Returns all the projects names and codes as a long CSV string'''

        projects = self.jira.projects()
        csv = "Name;Code\n"

        for i in projects:
            csv += i.name + ";" + i.key + "\n"

        return csv

    def getCustomFieldID(self, name):
        '''Getting all the current custom fields ID's and dump it to a CSV file for revision.'''

        # Fetch all fields
        fileManager = FileManager()
        allfields = self.jira.fields()

        # Make a map from field name -> field id
        nameMap = {field['name']: field['id'] for field in allfields}

        stringBuffer = StringIO()
        stringBuffer.write("Field Name;Code\n")

        for field in allfields:
            stringBuffer.write(field['name'] + ";" + field['id'] + "\n")

        fileManager.getSendToCSVFile(stringBuffer.getvalue())

        if (name != None):
            try:
                result = nameMap[name]
            except:
                return None
            return result
        else:
            return None
Пример #26
0
class ViraAPI():
    '''
    This class gets imported by __init__.py
    '''
    def __init__(self):
        '''
        Initialize vira
        '''

        # Load user-defined config files
        file_servers = vim.eval('g:vira_config_file_servers')
        file_projects = vim.eval('g:vira_config_file_projects')
        try:
            self.vira_servers = load_config(file_servers)
            self.vira_projects = load_config(file_projects)
        except:
            print(f'Could not load {file_servers} or {file_projects}')

        self.userconfig_filter_default = {
            'assignee': '',
            'component': '',
            'fixVersion': '',
            'issuetype': '',
            'priority': '',
            'project': '',
            'reporter': '',
            'status': '',
            "'Epic Link'": '',
            'statusCategory': ['To Do', 'In Progress'],
            'text': ''
        }
        self.reset_filters()

        self.userconfig_newissue = {
            'assignee': '',
            'component': '',
            'fixVersion': '',
            'issuetype': 'Bug',
            'priority': '',
            'epics': '',
            'status': '',
        }

        self.users = set()
        self.versions = set()
        self.servers = set()
        self.users_type = ''
        self.async_count = 0

        self.versions_hide(True)

    def _async(self, func):
        try:
            func()
        except:
            pass

    def _async_vim(self):
        #  TODO: VIRA-247 [210223] - Clean-up vim variables in python _async
        try:
            if len(vim.eval('s:versions')) == 0:
                vim.command('let s:projects = s:projects[1:]')
                if len(vim.eval('s:projects')) == 0:
                    #  TODO: VIRA-247 [210223] - Check for new projects and versions and start PRIORITY ranking for updates
                    vim.command('let s:vira_async_timer = g:vira_async_timer')
                    self.get_projects()
                self.get_versions()
            else:
                #  self.version_percent(str(vim.eval('s:projects[0]')), str(vim.eval('s:versions[0]')))
                vim.command('let s:versions = s:versions[1:]')

            if self.async_count == 0 and vim.eval(
                    's:vira_async_timer') == 10000:
                self.users = self.get_users()
                self.async_count = 1000
            self.async_count -= 1
        except:
            pass

    def create_issue(self, input_stripped):
        '''
        Create new issue in jira
        '''

        section = {
            'summary':
            parse_prompt_text(input_stripped, '*Summary*', 'Description'),
            'description':
            parse_prompt_text(input_stripped, 'Description', '*Project*'),
            'project':
            parse_prompt_text(input_stripped, '*Project*', '*IssueType*'),
            'issuetype':
            parse_prompt_text(input_stripped, '*IssueType*', 'Status'),
            'status':
            parse_prompt_text(input_stripped, 'Status', 'Priority'),
            'priority':
            parse_prompt_text(input_stripped, 'Priority', 'Component'),
            'components':
            parse_prompt_text(input_stripped, 'Component', 'Version'),
            'fixVersions':
            parse_prompt_text(input_stripped, 'Version', 'Assignee'),
            'assignee':
            parse_prompt_text(input_stripped, 'Assignee'),
        }

        # Check if required fields was entered by user
        if section['summary'] == '' or section['project'] == '' or section[
                'issuetype'] == '':
            return

        issue_kwargs = {
            'project': section['project'],
            'summary': section['summary'],
            'description': section['description'],
            'issuetype': {
                'name': section['issuetype']
            },
            'priority': {
                'name': section['priority']
            },
            'components': [{
                'name': section['components']
            }],
            'fixVersions': [{
                'name': section['fixVersions']
            }],
            'assignee': {
                'name': section['assignee']
            },
        }

        # Jira API doesn't accept empty fields for certain keys
        for key in issue_kwargs.copy().keys():
            if section[key] == '':
                issue_kwargs.pop(key)

        # Create issue and transition
        issue_key = self.jira.create_issue(**issue_kwargs)
        if section['status'] != '':
            self.jira.transition_issue(issue_key, section['status'])

        jira_server = vim.eval('g:vira_serv')
        print(f'Added {jira_server}/browse/{issue_key}')

    def add_worklog(self, issue, timeSpentSeconds, comment):
        '''
        Calculate the offset for the start time of the time tracking
        '''

        earlier = datetime.now() - datetime.timedelta(seconds=timeSpentSeconds)

        self.jira.add_worklog(issue=issue,
                              timeSpentSeconds=timeSpentSeconds,
                              comment=comment,
                              started=earlier)

    def connect(self, server):
        '''
        Connect to Jira server with supplied auth details
        '''

        self.users = set()
        self.versions = set()
        self.users_type = ''

        try:
            # Specify whether the server's TLS certificate needs to be verified
            if self.vira_servers[server].get('skip_cert_verify'):
                urllib3.disable_warnings(
                    urllib3.exceptions.InsecureRequestWarning)
                cert_verify = False
            else:
                cert_verify = True

            # Get auth for current server
            username = self.vira_servers[server].get('username')
            password_cmd = self.vira_servers[server].get('password_cmd')
            if password_cmd:
                password = run_command(password_cmd)['stdout'].strip().split(
                    '\n')[0]
            else:
                password = self.vira_servers[server]['password']
        except:
            cert_verify = True
            server = vim.eval('input("server: ")')
            vim.command('let g:vira_serv = "' + server + '"')
            username = vim.eval('input("username: "******"password: "******"' + server + '"')

            # Authorize
            self.jira = JIRA(options={
                'server': server,
                'verify': cert_verify,
            },
                             basic_auth=(username, password),
                             timeout=2,
                             async_=True,
                             max_retries=2)

            # Initial list updates
            self.users = self.get_users()
            self.get_projects()
            self.get_versions()

            vim.command('echo "Connection to jira server was successful"')
        except JIRAError as e:
            if 'CAPTCHA' in str(e):
                vim.command(
                    'echo "Could not log into jira! Check authentication details and log in from web browser to enter mandatory CAPTCHA."'
                )
            else:
                #  vim.command('echo "' + str(e) + '"')
                vim.command('let g:vira_serv = ""')
                #  raise e
        except:
            vim.command('let g:vira_serv = ""')
            vim.command(
                'echo "Could not log into jira! See the README for vira_server.json information"'
            )

    def filter_str(self, filterType):
        '''
        Build a filter string to add to a JQL query
        The string will look similar to one of these:
            AND status in ('In Progress')
            AND status in ('In Progress', 'To Do')
        '''

        if self.userconfig_filter.get(filterType, '') == '':
            return

        selection = str(
            self.userconfig_filter[filterType]).strip('[]') if type(
                self.userconfig_filter[filterType]
            ) == list else self.userconfig_filter[filterType] if type(
                self.userconfig_filter[filterType]
            ) == tuple else "'" + self.userconfig_filter[filterType] + "'"

        return str(f"{filterType} in ({selection})").replace(
            "'None'", "Null").replace("'Unassigned'", "Null").replace(
                "'currentUser'", "currentUser()").replace(
                    "'currentUser()'", "currentUser()").replace(
                        "'currentuser'", "currentUser()").replace(
                            "'currentuser()'",
                            "currentUser()").replace("'null'", "Null").replace(
                                f"text in ({selection})",
                                f"text ~ {selection}")

    def get_assign_issue(self):
        '''
        Menu to select users
        '''

        self.print_users()

    def get_assignees(self):
        '''
        Get my issues with JQL
        '''

        self.print_users()

    def get_comments(self, issue):
        '''
        Get all the comments for an issue
        '''

        # Get the issue requested
        issues = self.jira.search_issues('issue = "' + issue.key + '"',
                                         fields='summary,comment',
                                         json_result='True')

        # Loop through all of the comments
        comments = ''
        for comment in issues["issues"][0]["fields"]["comment"]["comments"]:
            comments += (f"{comment['author']['displayName']}" + ' | ',
                         f"{comment['updated'][0:10]}" + ' @ ',
                         f"{comment['updated'][11:16]}" + ' | ',
                         f"{comment['body']} + '\n'")

        return comments

    def get_components(self):
        '''
        Build a vim pop-up menu for a list of components
        '''

        for component in self.jira.project_components(
                self.userconfig_filter['project']):
            print(component.name)
        print('None')

    def get_component(self):
        '''
        Build a vim pop-up menu for a list of components
        '''

        self.get_components()

    def get_epic(self):
        self.get_epics()

    def get_epics(self):
        '''
        Get my issues with JQL
        '''
        hold = dict(self.userconfig_filter)
        project = self.userconfig_filter['project']
        self.reset_filters()
        self.userconfig_filter["issuetype"] = "Epic"
        self.userconfig_filter["project"] = project
        self.get_issues()
        print('None')
        self.userconfig_filter = hold

    def get_issue(self, issue):
        '''
        Get single issue by issue id
        '''

        return self.jira.issue(issue)

    def get_issues(self):
        '''
        Get my issues with JQL
        '''

        issues = []
        key_length = 0
        summary_length = 0
        issuetype_length = 0
        status_length = 4
        user_length = 0

        for issue in self.query_issues():
            fields = issue['fields']

            user = str(fields['assignee']['displayName']) if type(
                fields['assignee']) == dict else 'Unassigned'
            user_length = len(user) if len(user) > user_length else user_length

            key_length = len(
                issue['key']) if len(issue['key']) > key_length else key_length

            summary = fields['summary']
            summary_length = len(
                summary) if len(summary) > summary_length else summary_length

            issuetype = fields['issuetype']['name']
            issuetype_length = len(issuetype) if len(
                issuetype) > issuetype_length else issuetype_length

            status = fields['status']['name']
            status_length = len(
                status) if len(status) > status_length else status_length

            issues.append([
                issue['key'], fields['summary'], fields['issuetype']['name'],
                fields['status']['name'], user
            ])

        # Add min/max limits on summary length
        columns = vim.eval("&columns")
        min_summary_length = 25
        max_summary_length = int(
            columns) - key_length - issuetype_length - status_length - 28
        summary_length = min_summary_length if max_summary_length < min_summary_length else max_summary_length if summary_length > max_summary_length else summary_length

        for issue in issues:
            print(('{: <' + str(key_length) + '}').format(issue[0]) + " │ " +
                  ('{: <' + str(summary_length) +
                   '}').format(issue[1][:summary_length]) + "  │ " +
                  ('{: <' + str(issuetype_length) + '}').format(issue[2]) +
                  " │ " +
                  ('{: <' + str(status_length) + '}').format(issue[3]) +
                  ' │ ' + issue[4])

    def get_issuetypes(self):
        '''
        Get my issues with JQL
        '''

        for issuetype in self.jira.issue_types():
            print(issuetype)

    def get_issuetype(self):
        '''
        Get my issues with JQL
        '''

        for issuetype in self.jira.issue_types():
            print(issuetype)

    def get_priorities(self):
        '''
        Get my issues with JQL
        '''

        for priority in self.jira.priorities():
            print(priority)

    def print_projects(self):
        '''
        Build a vim pop-up menu for a list of projects
        '''

        all_projects = self.get_projects()
        batch_size = 10
        project_batches = [
            all_projects[i:i + batch_size]
            for i in range(0, len(all_projects), batch_size)
        ]

        for batch in project_batches:
            projects = self.jira.createmeta(projectKeys=','.join(batch),
                                            expand='projects')['projects']
            [print(p['key'] + ' ~ ' + p['name']) for p in projects]

    def get_projects(self):
        '''
        Build a vim pop-up menu for a list of projects
        '''

        # Project filter for version list
        self.projects = []
        for project in self.jira.projects():
            self.projects.append(str(project))
        vim.command('let s:projects = ' + str(self.projects))

        return self.projects

    def get_priority(self):
        '''
        Build a vim pop-up menu for a list of projects
        '''

        self.get_priorities()

    def get_prompt_text(self, prompt_type, comment_id=None):
        '''
        Get prompt text used for inputting text into jira
        '''

        self.prompt_type = prompt_type

        # Edit filters
        if prompt_type == 'edit_filter':
            self.prompt_text_commented = '\n# Edit all filters in JSON format'
            self.prompt_text = json.dumps(
                self.userconfig_filter,
                indent=True) + self.prompt_text_commented
            return self.prompt_text

        # Edit summary
        active_issue = vim.eval("g:vira_active_issue")
        if prompt_type == 'summary':
            self.prompt_text_commented = '\n# Edit issue summary'
            summary = self.jira.search_issues(
                'issue = "' + active_issue + '"',
                fields=','.join(['summary']),
                json_result='True')['issues'][0]['fields']['summary']
            self.prompt_text = summary + self.prompt_text_commented
            return self.prompt_text

        # Edit description
        if prompt_type == 'description':
            self.prompt_text_commented = '\n# Edit issue description'
            description = self.jira.search_issues(
                'issue = "' + active_issue + '"',
                fields=','.join(['description']),
                json_result='True')['issues'][0]['fields'].get('description')
            if description:
                description = description.replace('\r\n', '\n')
            else:
                description = ''
            self.prompt_text = description + self.prompt_text_commented
            return self.prompt_text

        self.prompt_text_commented = '''
# ---------------------------------
# Please enter text above this line
# An empty message will abort the operation.
#
# Below is a list of acceptable values for each input field.
#
# Users:'''
        for user in self.users:
            user = user.split(' ~ ')
            name = user[0]
            id = user[1]
            if self.users_type == 'accountId':
                self.prompt_text_commented += f'''
# [{name}|~accountid:{id}]'''
            else:
                self.prompt_text_commented += f'''
# [~{id}]'''

        # Add comment
        if self.prompt_type == 'add_comment':
            self.prompt_text = self.prompt_text_commented
            return self.prompt_text

        # Edit comment
        if self.prompt_type == 'edit_comment':
            self.active_comment = self.jira.comment(active_issue, comment_id)
            self.prompt_text = self.active_comment.body + self.prompt_text_commented
            return self.prompt_text

        statuses = [x.name for x in self.jira.statuses()]
        issuetypes = [x.name for x in self.jira.issue_types()]
        priorities = [x.name for x in self.jira.priorities()]
        components = [
            x.name for x in self.jira.project_components(
                self.userconfig_filter['project'])
        ] if self.userconfig_filter['project'] != '' else ''
        versions = [
            x.name for x in self.jira.project_versions(
                self.userconfig_filter['project'])
        ] if self.userconfig_filter['project'] != '' else ''
        projects = [x.key for x in self.jira.projects()]

        # Extra info for prompt_type == 'issue'
        self.prompt_text_commented += f'''
#
# Projects: {projects}
# IssueTypes: {issuetypes}
# Statuses: {statuses}
# Priorities: {priorities}
# Components in {self.userconfig_filter["project"]} Project: {components}
# Versions in {self.userconfig_filter["project"]} Project: {versions}'''

        self.prompt_text = f'''[*Summary*]
[Description]

[*Project*] {self.userconfig_filter["project"]}
[*IssueType*] {self.userconfig_newissue["issuetype"]}
[Status] {self.userconfig_newissue["status"]}
[Priority] {self.userconfig_newissue["priority"]}
[Component] {self.userconfig_newissue["component"]}
[Version] {self.userconfig_newissue["fixVersion"]}
[Assignee] {self.userconfig_newissue["assignee"]}
{self.prompt_text_commented}'''
        return self.prompt_text

    def format_date(self, date):
        time = datetime.now().strptime(date,
                                       '%Y-%m-%dT%H:%M:%S.%f%z').astimezone()
        return str(time)[0:10] + ' ' + str(time)[11:16]

    def get_report(self):
        '''
        Print a report for the given issue
        '''

        for customfield in self.jira.fields():
            if customfield['name'] == 'Epic Link':
                epicID = customfield['id']

        # Get passed issue content
        active_issue = vim.eval("g:vira_active_issue")
        issues = self.jira.search_issues(
            'issue = "' + active_issue + '"',
            fields=','.join([
                'project', 'summary', 'comment', 'component', 'description',
                'issuetype', 'priority', 'status', 'created', 'updated',
                'assignee', 'reporter', 'fixVersion', 'customfield_10106',
                'labels', epicID
            ]),
            json_result='True')
        issue = issues['issues'][0]['fields']

        # Prepare report data
        open_fold = '{{{'
        close_fold = '}}}'
        summary = issue['summary']
        story_points = str(issue.get('customfield_10106', ''))
        created = self.format_date(issue['created'])
        updated = self.format_date(issue['updated'])
        issuetype = issue['issuetype']['name']
        status = issue['status']['name']
        priority = issue['priority']['name']
        assignee = issue['assignee']['displayName'] if type(
            issue['assignee']) == dict else 'Unassigned'
        reporter = issue['reporter']['displayName']
        component = ', '.join([c['name'] for c in issue['components']])
        version = ', '.join([v['name'] for v in issue['fixVersions']])
        epics = str(issue.get(epicID))
        vim.command(f'let s:vira_epic_field = "{epicID}"')
        description = str(issue.get('description'))

        # Version percent for single version attacted
        #  if len(issue['fixVersions']) == 1 and version != '':
        #  version += ' | ' + self.version_percent(
        #  str(issue['project']['key']), version) + '%'

        comments = ''
        idx = 0
        for idx, comment in enumerate((issue['comment']['comments'])):
            comments += ''.join([
                comment['author']['displayName'] + ' @ ' +
                self.format_date(comment['updated']) + ' {{' + '{2\n' +
                comment['body'] + '\n}}}\n'
            ])
        old_count = idx - 3
        old_comment = 'Comment' if old_count == 1 else 'Comments'
        comments = ''.join(
            [str(old_count) + ' Older ' + old_comment +
             ' {{{1\n']) + comments if old_count >= 1 else comments
        comments = comments.replace('}}}', '}}}}}}', idx - 3)
        comments = comments.replace('}}}}}}', '}}}', idx - 4)

        # Find the length of the longest word [-1]
        words = [
            created, updated, issuetype, status, story_points, priority,
            component, version, assignee, reporter, epics
        ]
        wordslength = sorted(words, key=len)[-1]
        s = '─'
        dashlength = s.join([char * len(wordslength) for char in s])

        active_issue_spacing = int((16 + len(dashlength)) / 2 -
                                   len(active_issue) / 2)
        active_issue_spaces = ' '.join(
            [char * (active_issue_spacing) for char in ' '])
        active_issue_space = ' '.join([
            char * ((len(active_issue) + len(dashlength)) % 2) for char in ' '
        ])

        created_spaces = ' '.join(
            [char * (len(dashlength) - len(created)) for char in ' '])
        updated_spaces = ' '.join(
            [char * (len(dashlength) - len(updated)) for char in ' '])
        task_type_spaces = ' '.join(
            [char * (len(dashlength) - len(issuetype)) for char in ' '])
        status_spaces = ' '.join(
            [char * (len(dashlength) - len(status)) for char in ' '])
        story_points_spaces = ''.join(
            [char * (len(dashlength) - len(story_points)) for char in ' '])
        priority_spaces = ''.join(
            [char * (len(dashlength) - len(priority)) for char in ' '])
        component_spaces = ''.join(
            [char * (len(dashlength) - len(component)) for char in ' '])
        version_spaces = ''.join(
            [char * (len(dashlength) - len(version)) for char in ' '])
        assignee_spaces = ''.join(
            [char * (len(dashlength) - len(assignee)) for char in ' '])
        reporter_spaces = ''.join(
            [char * (len(dashlength) - len(reporter)) for char in ' '])
        epics_spaces = ''.join(
            [char * (len(dashlength) - len(epics)) for char in ' '])

        # Create report template and fill with data
        report = '''┌────────────────{dashlength}─┐
│{active_issue_spaces}{active_issue}{active_issue_spaces}{active_issue_space} │
├──────────────┬─{dashlength}─┤
│      Created │ {created}{created_spaces} │
│      Updated │ {updated}{updated_spaces} │
│         Type │ {issuetype}{task_type_spaces} │
│       Status │ {status}{status_spaces} │
│ Story Points │ {story_points}{story_points_spaces} │
│     Priority │ {priority}{priority_spaces} │
│    Epic Link │ {epics}{epics_spaces} │
│ Component(s) │ {component}{component_spaces} │
│   Version(s) │ {version}{version_spaces} │
│     Assignee │ {assignee}{assignee_spaces} │
│     Reporter │ {reporter}{reporter_spaces} │
└──────────────┴─{dashlength}─┘
┌──────────────┐
│    Summary   │
└──────────────┘
{summary}

┌──────────────┐
│  Description │
└──────────────┘
{description}

┌──────────────┐
│   Comments   │
└──────────────┘
{comments}'''

        self.set_report_lines(report, description, issue)

        self.prompt_text = self.report_users(report.format(**locals()))
        return self.prompt_text

    def report_users(self, report):
        '''
        Replace report accountid with names
        '''

        for user in self.users:
            user = user.split(' ~ ')
            if user[0] != "Unassigned":
                report = report.replace('accountid:',
                                        '').replace('[~' + user[1] + ']',
                                                    '[~' + user[0] + ']')

        return report

    def get_reporters(self):
        '''
        Get my issues with JQL
        '''

        self.print_users()

    def get_servers(self):
        '''
        Get list of servers
        '''

        try:
            for server in self.vira_servers.keys():
                print(server)
            print('Null')
        except:
            self.connect('')

    def get_statuses(self):
        '''
        Get my issues with JQL
        '''

        statuses = []
        for status in self.jira.statuses():
            if str(status) not in statuses:
                statuses.append(str(status))
                print(str(status))

    def get_set_status(self):
        '''
        Get my issues with JQL
        '''

        self.get_statuses()

    def get_version(self):
        '''
        Get my issues with JQL
        '''

        self.print_versions()

    def new_component(self, name, project):
        '''
        New component added to project
        '''

        self.jira.create_component(name=name,
                                   project=project,
                                   description=name)

    def new_version(self, name, project, description):
        '''
        Get my issues with JQL
        '''

        self.jira.create_version(name=name,
                                 project=project,
                                 description=description)

    def print_users(self):
        '''
        Print users
        '''

        print(self.get_current_user() + ' ~ currentUser')
        for user in self.users:
            print(user)
        print('Unassigned')

    def get_users(self):
        '''
        Get my issues with JQL
        '''

        query = 'ORDER BY updated DESC'
        issues = self.jira.search_issues(query,
                                         fields='assignee, reporter',
                                         json_result='True',
                                         maxResults=-1)

        # Determine cloud/server jira
        self.users_type = 'accountId' if issues['issues'][0]['fields'][
            'reporter'].get('accountId') else 'name'

        for issue in issues['issues']:
            user = str(issue['fields']['reporter']['displayName']
                       ) + ' ~ ' + issue['fields']['reporter'][self.users_type]
            self.users.add(user)
            if type(issue['fields']['assignee']) == dict:
                user = str(
                    issue['fields']['assignee']['displayName']
                ) + ' ~ ' + issue['fields']['assignee'][self.users_type]
            self.users.add(user)

        return sorted(self.users)

    def get_current_user(self):
        query = 'reporter = currentUser() or assignee = currentUser()'
        issues = self.jira.search_issues(query,
                                         fields='assignee, reporter',
                                         json_result='True',
                                         maxResults=-1)

        issue = issues['issues'][0]['fields']
        return str(issue['assignee'][self.users_type] if type(
            issue['assignee']) == dict else issue['reporter'][self.users_type]
                   if type(issue['reporter']) == dict else 'Unassigned')

    def print_versions(self):
        '''
        Print version list with project filters
        '''
        try:
            versions = sorted(self.versions)
            wordslength = sorted(versions, key=len)[-1]
            s = ' '
            dashlength = s.join([char * len(wordslength) for char in s])
            for version in versions:
                print(
                    version.split('|')[0] + ''.join([
                        char * (len(dashlength) - len(version)) for char in ' '
                    ]) + '   ' + version.split('|')[1] + ' ' +
                    version.split('|')[2])
        except:
            pass
        print('None')

    def version_percent(self, project, fixVersion):
        project = str(project)
        fixVersion = str(fixVersion)
        if str(project) != '[]' and str(project) != '' and str(
                fixVersion) != '[]' and str(fixVersion) != '':
            query = 'fixVersion = ' + fixVersion + ' AND project = "' + project + '"'
            issues = self.jira.search_issues(query,
                                             fields='fixVersion',
                                             json_result='True',
                                             maxResults=1)

            try:
                issue = issues['issues'][0]['fields']['fixVersions'][0]
                idx = issue['id']

                total = self.jira.version_count_related_issues(
                    idx)['issuesFixedCount']
                pending = self.jira.version_count_unresolved_issues(idx)
                fixed = total - pending
                percent = str(round(fixed / total *
                                    100, 1)) if total != 0 else 1
                space = ''.join([char * (5 - len(percent)) for char in ' '])

                name = fixVersion
                try:
                    description = issue['description']
                except:
                    description = 'None'
                    pass
            except:
                total = 0
                pending = 0
                fixed = total - pending
                percent = "0"
                space = ''.join([char * (5 - len(percent)) for char in ' '])
                name = fixVersion
                description = ''
                pass

            version = str(
                str(name) + ' ~ ' + str(description) + '|' + str(fixed) + '/' +
                str(total) + space + '|' + str(percent) + '%')

            self.versions_hide = vim.eval('g:vira_version_hide')
            if fixed != total or total == 0 or not int(
                    self.versions_hide) == 1:
                self.versions.add(
                    str(project) + ' ~ ' + str(version.replace('\'', '')))

        else:
            percent = 0

        return percent

    def get_versions(self):
        '''
        Build a vim pop-up menu for a list of versions with project filters
        '''

        # Loop through each project and all versions within
        try:
            for v in reversed(
                    self.jira.project_versions(vim.eval('s:projects[0]'))):
                vim.command('let s:versions = add(s:versions,\"' + str(v) +
                            '\")')
        except:
            vim.command('let s:versions = []')

    def load_project_config(self, repo):
        '''
        Load project configuration for the current git repo
        The current repo can either be determined by current files path
        or by the user setting g:vira_repo (part of :ViraLoadProject)

        For example, an entry in projects.yaml may be:

        vira:
          server: https://jira.tgall.ca
          project_name: VIRA
        '''

        # Only proceed if projects file parsed successfully
        if not getattr(self, 'vira_projects', None):
            return

        # If current repo/folder doesn't exist, use __default__ project config if it exists
        if repo == '':
            repo = run_command(
                'git rev-parse --show-toplevel')['stdout'].strip()
            if not self.vira_projects.get(repo):
                repo = repo.split('/')[-1]
            if not self.vira_projects.get(repo):
                repo = run_command('pwd')['stdout'].strip()
            if not self.vira_projects.get(repo):
                repo = repo.split('/')[-1]
            if not self.vira_projects.get(repo):
                repo = '__default__'
            if not self.vira_projects.get('__default__'):
                return

        # Set server
        server = self.vira_projects.get(repo, {}).get('server')
        if server:
            vim.command(f'let g:vira_serv = "{server}"')

        # Set user-defined filters for current project
        for key in self.userconfig_filter.keys():
            value = self.vira_projects.get(repo, {}).get('filter', {}).get(key)
            if value:
                self.userconfig_filter[key] = value

        # Set user-defined new-issue defaults for current project
        for key in self.userconfig_newissue.keys():
            value = self.vira_projects.get(repo, {}).get('newissue',
                                                         {}).get(key)
            if value:
                self.userconfig_newissue[key] = value

        # Set user-defined issue sort options
        sort_order = self.vira_projects.get(repo,
                                            {}).get('issuesort',
                                                    'updated DESC')
        self.userconfig_issuesort = ', '.join(sort_order) if type(
            sort_order) == list else sort_order

    def query_issues(self):
        '''
        Query issues based on current filters
        '''

        q = []
        for filterType in self.userconfig_filter.keys():
            filter_str = self.filter_str(filterType)
            if filter_str:
                q.append(filter_str)

        query = ' AND '.join(q) + ' ORDER BY ' + self.userconfig_issuesort
        issues = self.jira.search_issues(
            query,
            fields='summary,comment,status,statusCategory,issuetype,assignee',
            json_result='True',
            maxResults=vim.eval('g:vira_issue_limit'))

        return issues['issues']

    def reset_filters(self):
        '''
        Reset filters to their default values
        '''

        self.userconfig_filter = dict(self.userconfig_filter_default)

    def set_report_lines(self, report, description, issue):
        '''
        Create dictionary for vira report that shows relationship
        between line numbers and fields to be edited
        '''

        writable_fields = {
            'Assignee': 'ViraSetAssignee',
            'Component': 'ViraSetComponent',
            'Priority': 'ViraSetPriority',
            'Epic Link': 'ViraSetEpic',
            'Status': 'ViraSetStatus',
            'Type': 'ViraSetType',
            'Version': 'ViraSetVersion',
        }

        self.report_lines = {}

        for idx, line in enumerate(report.split('\n')):
            for field, command in writable_fields.items():
                if field in line:
                    self.report_lines[idx + 1] = command
                    continue

        for x in range(16, 21):
            self.report_lines[x] = 'ViraEditSummary'

        description_len = description.count('\n') + 3
        for x in range(21, 23 + description_len):
            self.report_lines[x] = 'ViraEditDescription'

        offset = 2 if len(issue['comment']['comments']) > 4 else 1
        comment_line = 25 + description_len + offset
        for comment in issue['comment']['comments']:
            comment_len = comment['body'].count('\n') + 3
            for x in range(comment_line, comment_line + comment_len):
                self.report_lines[x] = 'ViraEditComment ' + comment['id']
            comment_line = comment_line + comment_len

    def set_prompt_text(self):
        '''
        Take the user prompt text and perform an action
        Usually, this involves writing to the jira server
        '''

        # User input
        issue = vim.eval('g:vira_active_issue')
        userinput = vim.eval('g:vira_input_text')
        input_stripped = userinput.replace(self.prompt_text_commented.strip(),
                                           '').strip()

        # Check if anything was actually entered by user
        if input_stripped == '' or userinput.strip() == self.prompt_text.strip(
        ):
            print("No vira actions performed")
            return

        if self.prompt_type == 'edit_filter':
            self.userconfig_filter = json.loads(input_stripped)
        elif self.prompt_type == 'add_comment':
            self.jira.add_comment(issue, input_stripped)
        elif self.prompt_type == 'edit_comment':
            self.active_comment.update(body=input_stripped)
        elif self.prompt_type == 'summary':
            self.jira.issue(issue).update(summary=input_stripped)
        elif self.prompt_type == 'description':
            self.jira.issue(issue).update(description=input_stripped)
        elif self.prompt_type == 'issue':
            self.create_issue(input_stripped)

    def versions_hide(self, state):
        '''
        Display and hide complete versions
        '''

        if state is True or 1 or 'ture' or 'True':
            self.version_hide = True
        elif state is False or 0 or 'false' or 'False':
            self.version_hide = False
        else:
            self.version_hide = not self.version_hide
            print("description     : ", str(product['description']))
            comps = bzapi.getcomponents(product['name'])
            if comps is not None:
                #       description     :
                print("components (-c) :")
            for comp in comps:
                print("    ", str(comp))
    exit(0)

#
# JIRA
#
jira = JIRA('https://jira.iotivity.org')
projects = jira.projects()

resp = jira.fields()
fmap = {}
#fr i in resp:
# print ( i.id, id.name)
#pp.pprint (resp)
for i in resp:
    field_name = i[u'name'].encode('ascii', 'ignore')
    field_id = i[u'id'].encode('ascii', 'ignore')
    fmap[field_name] = field_id
#if args.verbose:
#    pprint.pprint(fmap)

#print (projects)
#allfields=jira.fields()
#nameMap = {field['name']:field['id'] for field in allfields}
class JiraAnalysis():
    def __init__(self, jira_url, jira_username, jira_token, jira_squad_labels):
        self.issue_cache = {}
        self.jira = JIRA(jira_url, basic_auth=(jira_username, jira_token))
        self.jira_squad_labels = jira_squad_labels
        self.sprint_field = self.get_custom_field_key('Sprint')
        self.story_point_field = self.get_custom_field_key('Story Points')

        if self.sprint_field is None:
            raise Exception("Failed to find Sprint Field")

        if self.story_point_field is None:
            raise Exception("Failed to find Story Point Field")

    # Retrieve the custom field matching to a particular name since JIRA gives custom fields a random ID
    def get_custom_field_key(self, name):
        all_fields = self.jira.fields()
        for field in all_fields:
            if field['name'] == name:
                return field['key']
        return None

    # Retrieve the squad for an issue - based on labels
    def get_squad(self, issue):
        for label in issue.fields.labels:
            for squad_label in self.jira_squad_labels:
                if squad_label.lower() == label.lower():
                    return squad_label
        return None

    # Get issue type, a bit weird since it's off of fields and needs to be converted to string
    def get_issue_type(self, issue):
        return issue.fields.issuetype.name.lower()

    # Get description from an issue
    def get_description(self, issue):
        return issue.fields.description

    # Get story points from an issue
    def get_story_points(self, issue):
        return get_or_float_zero(issue.fields, self.story_point_field)

    # Get the priority of an issue
    def get_priority(self, issue):
        for label in issue.fields.labels:
            # TODO: Update this for other formats
            # Take the first label found to avoid double counting
            if '2018:q1:' in label.lower() or '2018:q2' in label.lower():
                try:
                    return int(label.split(':')[-1])
                except:
                    return 100
        # The Misc priority
        return 100

    # For a set of issues get stats by priority
    def get_priority_stats(self, issues):
        priority_count = Counter()
        priority_story_points = defaultdict(float)
        no_priority_stories = []
        for issue in issues:
            priority = self.get_priority(issue)
            if priority is not None:
                story_points = self.get_story_points(issue)
                priority_count.update([priority])
                priority_story_points[priority] += float(story_points)
            else:
                no_priority_stories.append(issue)
        return priority_count, priority_story_points, no_priority_stories

    # Wrap the pagination code so user doesn't have to do it themselves
    def get_issues(self, query):
        if query in self.issue_cache:
            return self.issue_cache[query]

        all_issues = []
        MAX_RESULTS = 100
        issues = self.jira.search_issues(query, maxResults=MAX_RESULTS)
        total = issues.total
        all_issues.extend(list(issues))
        if total > MAX_RESULTS:  # Actually need to paginate
            for page in range(1, int(ceil(1.0 * total / MAX_RESULTS))):
                logger.info('Getting page %s', page)
                issues = self.jira.search_issues(query,
                                                 maxResults=MAX_RESULTS,
                                                 startAt=page * MAX_RESULTS)
                logger.info('Retrieved %s issues', len(issues))
                all_issues.extend(list(issues))
        logger.info('Total retrieved %s', len(all_issues))
        self.issue_cache[query] = all_issues
        return all_issues

    # Clean up and write issues to a CSV
    def write_issues(self, start_date, end_date, fn):
        issues = self.get_issues(self.get_issue_query(start_date, end_date))
        with open(fn, 'w') as f:
            w = csv.writer(f)
            w.writerow([
                "ticket", "summary", "squad", "priority", "story_points",
                "assignee", "resolved_date", "type"
            ])
            for issue in issues:
                w.writerow([
                    issue,
                    issue.fields.summary.encode('utf-8'),
                    self.get_squad(issue),
                    self.get_priority(issue),
                    self.get_story_points(issue),
                    issue.fields.assignee if issue.fields.assignee else 'None',
                    issue.fields.resolutiondate,
                    self.get_issue_type(issue)
                ])

    # Get all done stories and bugs between a date range
    def get_issue_query(self, start_date, end_date):
        return 'project = "TL" and status = Done and resolutiondate >= "' + start_date + '" and resolutiondate <= "' + end_date + '" AND type in ("story", "bug", "task", "spike", "access")'

    # Just get the list of words
    def get_descriptions_words(self, start_date, end_date):
        issues = self.get_issues(self.get_issue_query(start_date, end_date))
        words = []
        for issue in issues:
            desc = self.get_description(issue)
            if desc is not None:
                words.extend(desc.split(' '))
        return words

    # Measure analytics per priority
    def analyze_priorities(self, start_date, end_date):
        issues = self.get_issues(self.get_issue_query(start_date, end_date))
        priority_count, priority_story_points, no_priority_stories = self.get_priority_stats(
            issues)

        logger.info('Priority counts')
        logger.info(print_dict(priority_count, '\n'))

        logger.info('Priority story points')
        logger.info(print_dict(priority_story_points, '\n'))

        logger.info('No priorities')
        for issue in no_priority_stories:
            logger.info("\t %s %s", issue, issue.fields.summary)

    # Measrure # of sprints to do a story
    def analyze_sprint_lag(self, start_date, end_date):
        squad_sprint_counts = defaultdict(list)
        squad_sprint_story_point_sum = defaultdict(float)
        squad_story_point_sum = defaultdict(float)
        squad_bugs = defaultdict(int)
        issues = self.get_issues(self.get_issue_query(start_date, end_date))
        for issue in issues:
            squad = self.get_squad(issue)
            try:
                num_sprints = len(getattr(issue.fields, self.sprint_field))
            except:
                num_sprints = 0
            story_points = get_or_float_zero(issue.fields,
                                             self.story_point_field)
            issue_type = self.get_issue_type(issue)

            # Has a squad and was actually done via sprint process
            if squad and num_sprints > 0 and issue_type == 'story':
                squad_sprint_counts[squad].append(num_sprints)

                if story_points > 0:
                    squad_sprint_story_point_sum[
                        squad] += num_sprints * story_points
                    squad_story_point_sum[squad] += story_points

            if issue_type == 'bug':
                squad_bugs[squad] += 1

        logger.info('Squad\tSprint Lag\tSP Sprint Lag\tBugs')
        for squad, counts in squad_sprint_counts.items():
            logger.info(
                '%s\t%s\t%s\t%s', squad,
                sum(counts) * 1.0 / len(counts),
                squad_sprint_story_point_sum[squad] /
                squad_story_point_sum[squad], squad_bugs[squad])

    # Measure # of story points done per assignee
    def analyze_story_points(self, start_date, end_date):
        user_story_point_sum = Counter()
        user_data = {}
        issues = self.get_issues(self.get_issue_query(start_date, end_date))
        for issue in issues:
            story_points = get_or_float_zero(issue.fields,
                                             self.story_point_field)
            assignee = str(
                issue.fields.assignee) if issue.fields.assignee else 'None'
            user_story_point_sum.update({assignee: int(story_points)})
            if assignee not in user_data:
                user_data[assignee] = defaultdict(int)
            issue_type = self.get_issue_type(issue)
            user_data[assignee][issue_type + '_cnt'] += 1
            user_data[assignee][issue_type +
                                '_story_points'] += int(story_points)

        logger.info('User\tSP\tStories\tBugs')
        for user, story_points in user_story_point_sum.most_common(100):
            logger.info('%s\t%s\t%s', user, story_points, user_data[user])
Пример #29
0
#If build user is not in priority list
else:
    #jira url
    server = 'https://example.atlassian.net'
    options = { 'server': server }

    jira = JIRA(options, basic_auth=(user,apikey))  #Jira Api authentication
    try:
        issue = jira.issue(JiraTicketId) #Fetch Ticket from ticket ID
        getassignee = issue.fields.assignee.displayName       #Fetching assignee from ticket
        assign = unicodedata.normalize('NFKD', getassignee).encode('ascii','ignore')
        status = issue.fields.status
        ticketType = issue.fields.issuetype

        #Fetching Severity field by name
        allfields = jira.fields()
        nameMap = {jira.field['name']:jira.field['id'] for jira.field in allfields}
        getvalue = getattr(issue.fields, nameMap["Severity"])
    except:
        print("{} Ticket ID is wrong or you don't have permission to see it.".format(stime))
    #---------------fetching email of assignee ----------
    def ExceljiraRead():
        try:
            #mentioned excel should have only read permision in jenkin server
            df = pd.read_excel('publickeyssh.xlsx', sheet_name='public-ssh-keys-final')
            jiraDisplayName = df['Employee Name']
            jmailID = df['Email address']
            for i in range(0,len(jmailID)-1):

                if jiraDisplayName[i]==assign:
                    jiramailid=jmailID[i]
Пример #30
0
class JiraService:
    def __init__(self):
        self.jira = JIRA(server=os.environ['JIRA_INSTANCE'], basic_auth=(os.environ['JIRA_USER'], os.environ['JIRA_PASSWORD']))
        self.hipchat = HypChat(os.environ['HIPCHAT_TOKEN'], endpoint = "https://puppet.hipchat.com")

        # map of <field-id> -> <field-name>, used to make querying custom fields more readable.
        self.name_map = {field['id'] : field['name'] for field in self.jira.fields()}

    # applies the passed-in JQL filter and returns an array of tickets that match it
    def get_tickets(self, search_filter):
        def process_ticket(ticket):
            ticket_fields = self.get_fields(ticket)
            return {
                "ticket" : ticket.key,
                "status" : ticket_fields["Status"]["name"]
            }

        return [process_ticket(ticket) for ticket in self.jira.search_issues(search_filter)]

    # returns a dictionary with the following keys:
    #   (1) involved_devs
    #         Nested structure with the following keys:
    #            -Everything that dev_info returns
    #            -category (Either "Assignee", "Watcher", or "Reporter", in that order)
    #   (2) team
    #         Lists the team associated with the ticket
    #
    # NOTE: Some puppet employees might have their e-mails end in @puppetlabs.com instead of
    # @puppet.com -- if this happens, then the code should be modified to try both e-mails when
    # acquiring the dev info.
    #
    # TODO: What if ticket does not have a field? E.g. No team, no assignee, etc. Handle this
    # case!
    def ticket_info(self, ticket):
        def involved_dev_info(involved_dev, category):
            dev_info = self.dev_info("*****@*****.**" % involved_dev['key'])
            dev_info['category'] = category
            return dev_info

        ticket_info = self.get_fields(self.jira.issue(ticket))
        ticket_watchers = self.jira.watchers(ticket)

        # calculate the involved devs
        (assignee, reporter) = tuple(
            [[involved_dev_info(ticket_info[dev_type], dev_type)] if ticket_info[dev_type] else [] for dev_type in ("Assignee", "Reporter")]
        )
        watchers = [involved_dev_info(watcher.raw, "Watcher") for watcher in ticket_watchers.watchers]
        return {
            "involved_devs" : assignee + watchers + reporter,
            "team" : ticket_info["Team"]["value"] if ticket_info["Team"] else None
        }

    # returns a dictionary with the following keys:
    #   (1) name
    #   (2) email
    #   (3) hipchat_alias
    def dev_info(self, dev_email):
        hipchat_info = self.hipchat.get_user(dev_email)
        return {
            'name' : hipchat_info['name'],
            'email' : dev_email,
            'hipchat_alias' : hipchat_info['mention_name']
        }

    # TODO: Make this private after everything's tested
    def get_fields(self, jira_issue):
        return {self.name_map[field_id] : field_value for field_id, field_value in jira_issue.raw['fields'].items()}