예제 #1
0
파일: vira_api.py 프로젝트: jamesl33/vira
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': '',
            'statusCategory': ['To Do', 'In Progress'],
            'text': ''
        }
        self.reset_filters()

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

    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.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
        '''

        # 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()
        else:
            password = self.vira_servers[server]['password']

        # Connect to jira server
        try:
            self.jira = JIRA(options={
                'server': server,
                'verify': cert_verify,
            },
                             basic_auth=(username, password),
                             timeout=5)
            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:
                raise e

    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(
            "'null'", "Null").replace("'Unassigned'",
                                      "Null").replace(f"text in ({selection})",
                                                      f"text ~ {selection}")

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

        self.get_users()

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

        self.get_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 popup menu for a list of components
        '''

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

    def get_component(self):
        '''
        Build a vim popup menu for a list of components
        '''

        self.get_components()

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

        for issue in self.query_issues(issuetypes="Epic"):
            print(issue["key"] + '  -  ' + issue["fields"]['summary'])

    def get_issue(self, issue):
        '''
        Get single issue by isuue 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 get_projects(self):
        '''
        Build a vim popup menu for a list of projects
        '''

        for project in self.jira.projects():
            print(project)

    def get_priority(self):
        '''
        Build a vim popup 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
        '''

        # Edit summary
        self.prompt_type = prompt_type
        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']
            return summary + self.prompt_text_commented

        # 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 = ''
            return description + self.prompt_text_commented

        # Prepare dynamic variables for prompt text
        query = 'ORDER BY updated DESC'
        issues = self.jira.search_issues(query,
                                         fields='assignee, reporter',
                                         json_result='True',
                                         maxResults=-1)

        # Determine cloud/server jira
        id = 'accountId' if issues['issues'][0]['fields']['reporter'].get(
            'accountId') else 'name'
        users = set()
        for issue in issues['issues']:
            user = str(issue['fields']['reporter']['displayName']
                       ) + ' ~ ' + issue['fields']['reporter'][id]
            users.add(user)
            if type(issue['fields']['assignee']) == dict:
                user = str(issue['fields']['assignee']['displayName']
                           ) + ' ~ ' + issue['fields']['assignee'][id]
            users.add(user)

        self.prompt_text_commented = f'''
# ---------------------------------
# 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: {users}
'''
        # Add comment
        if self.prompt_type == 'add_comment':
            return self.prompt_text_commented

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

        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}
'''
        return 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}'''

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

        # Get passed issue content
        active_issue = vim.eval("g:vira_active_issue")
        issues = self.jira.search_issues(
            'issue = "' + active_issue + '"',
            #  fields='*',
            fields=','.join([
                'summary', 'comment', 'component', 'description', 'issuetype',
                'priority', 'status', 'created', 'updated', 'assignee',
                'reporter', 'fixVersion', 'customfield_10106'
            ]),
            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 = issue['created'][0:10] + ' ' + issues['issues'][0]['fields'][
            'created'][11:16]
        updated = issue['updated'][0:10] + ' ' + issues['issues'][0]['fields'][
            'updated'][11:16]
        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']])
        description = str(issue.get('description'))

        comments = ''
        idx = 0
        for idx, comment in enumerate((issue['comment']['comments'])):
            comments += ''.join([
                comment['author']['displayName'] + ' @ ' +
                #  comment['body'] + '\n}}}\n'
                comment['updated'][0:10] + ' ' + comment['updated'][11:16] +
                ' {{{2\n' + comment['body'] + '\n}}}\n'
            ])
        comments = ''.join(['Old Comments {{{1\n'
                            ]) + comments if idx > 3 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
        ]
        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) % 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 ' '])

        # 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} │
│    Component │ {component}{component_spaces} │
│      Version │ {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)

        return report.format(**locals())

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

        self.get_users()

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

        for server in self.vira_servers.keys():
            print(server)

    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.get_versions()

    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)

        users = []
        for issue in issues["issues"]:

            id = str(issue['fields']['reporter']['self']).split("=")[1]
            user = issue['fields']['reporter']['displayName']
            if user + ' ~ ' + id not in users:
                users.append(user + ' ~ ' + str(id))

        for user in sorted(users):
            print(user)
        print('Unassigned')

    def get_versions(self):
        '''
        Build a vim popup menu for a list of versions
        '''

        for version in self.jira.project_versions(
                self.userconfig_filter['project']):
            print(version.name)
        print('null')

    def load_project_config(self):
        '''
        Load project configuration for the current git repo

        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

        repo = run_command(
            'git rev-parse --show-toplevel')['stdout'].strip().split('/')[-1]

        # If curren't repo doesn't exist, use __default__ project config if it exists
        if not self.vira_projects.get(repo):
            if self.vira_projects.get('__default__'):
                repo = '__default__'
            else:
                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

    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 updated DESC'
        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',
            'Status': 'ViraSetStatus',
            'Type': 'ViraSetType',
            'Version': 'ViraSetVersion',
            'Summary': 'ViraEditSummary',
        }

        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
                    if field == 'Summary':
                        self.report_lines[idx + 2] = command
                        self.report_lines[idx + 3] = command
                    continue

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

        offset = 2 if len(issue['comment']['comments']) > 4 else 1
        comment_line = 18 + 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 write_jira(self):
        '''
        Write to jira
        Can be issue name, description, comment, etc...
        '''

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

        # Check if anything was actually entered by user
        if input_stripped == '':
            print("No vira actions performed")
            return

        if self.prompt_type == 'add_comment':
            return self.jira.add_comment(issue, input_stripped)
        if self.prompt_type == 'edit_comment':
            return self.active_comment.update(body=input_stripped)
        elif self.prompt_type == 'summary':
            return self.jira.issue(issue).update(summary=input_stripped)
        elif self.prompt_type == 'description':
            return self.jira.issue(issue).update(description=input_stripped)
        elif self.prompt_type == 'issue':
            return self.create_issue(input_stripped)
예제 #2
0
class jiramenu():
    user = None
    project = None
    auth = None
    config = None
    debug = False
    r = Rofi()
    issues = []
    rofi_list = []

    def __init__(self, config, debug):
        self.config = config
        self.r.status("starting jiramenu")
        try:
            self.auth = JIRA(config['JIRA']['url'],
                             basic_auth=(config['JIRA']['user'],
                                         config['JIRA']['password']))
        except Exception as error:
            self.r.exit_with_error(str(error))
        self.debug = debug

    def log(self, text):
        if not self.debug:
            return
        print(text)

    def show(self, user):
        self.user = user
        self.project = self.config['JIRA']['project']
        if user:
            self.log(f"show issues for: {self.user}")

        query = self.config['JIRA']['query']
        if user:
            query += f" and assignee = '{user}'"
        if self.project:
            query += f" and project = '{self.project}'"
        self.log(f"Query: {query}")
        if not self.issues:
            self.issues = self.auth.search_issues(query)
            self.boards = self.auth.boards()

        if not self.rofi_list:
            if user:
                self.rofi_list.append("> all")
            else:
                self.rofi_list.append("> mine")
            self.issues.sort(key=lambda x: x.fields.status.id, reverse=False)
            for issue in self.issues:
                labels = ''
                if len(issue.fields.labels):
                    labels = '('
                    for idx, label in enumerate(issue.fields.labels):
                        labels += label
                        if idx != len(issue.fields.labels) -1:
                            labels += ', '
                    labels += ')'
                issuetext = ''
                issueassignee = ''
                initials = '  '
                if issue.fields.assignee:
                    issueassignee = issue.fields.assignee.displayName
                    initials = ''.join([x[0].upper() for x in issueassignee.split(' ')])
                if issue.fields.status.id == str(3):  #id:3 = Work in Progress
                    issuetext = '{WIP}'
                issuekey = issue.key
                issuekey = "{:<9}".format(issuekey)
                status = "{:<24}".format(issue.fields.status.name)

                issueassignee = "{:<20}".format(issueassignee)
                issuetext += f'{issuekey} {status} {initials}     {labels} {issue.fields.summary}'
                self.rofi_list.append(issuetext)

        # print active query plus number of results on top
        index, key = self.r.select(f'{query}[{len(self.rofi_list)}]',
                                   self.rofi_list,
                                   rofi_args=['-i'],
                                   width=100)
        del key
        if index < 0:
            exit(1)
        if index == 0:
            self.issues = []
            self.rofi_list = []
            if user:
                self.show(None)
            else:
                self.show(self.config['JIRA']['user'])
            return
        self.show_details(index, user)

    def addComment(self, ticket_number):
        comment = self.r.text_entry("Content of the comment:")
        if comment:
            # replace @user with [~user]
            comment = re.sub(r"@(\w+)", r"[~\1]", comment)
            self.auth.add_comment(ticket_number, comment)

    def show_details(self, index, user):
        inputIndex = index
        # ticket_number = re.match("IMP-([1-9]|[1-9][0-9])+", self.rofi_list[index]).group(0)
        issue = self.issues[index-1]
        ticket_number = issue.key
        summary = '-'.join(issue.fields.summary.split(' '))
        branch_name= ticket_number + '-' + summary[:33]

        self.log("[details]" + ticket_number)
        issue_description = issue.fields.description

        output = []
        output.append("> show in browser")
        output.append("")
        output.append(f"> copy branch ({branch_name})")
        output.append("")
        output.append("Status: " + self.issues[index - 1].fields.status.name)
        # output.append("Description: " + issue_description)
        description = []
        if issue_description:
            description = issue_description.split('\n')
        for item in description:
            output.append(item)

        if self.auth.comments(ticket_number):
            comment_ids = self.auth.comments(ticket_number)
            for comment_id in comment_ids:
                self.log("comment_id: " + str(comment_id))
                commentauthor = self.auth.comment(ticket_number, comment_id).author.displayName + ':'
                output.append(commentauthor)
                commenttext = self.auth.comment(ticket_number, comment_id).body
                commenttext = commenttext.split('\n')
                for line in commenttext:
                    output.append(line)
        else:
            output.append("no comments")
        output.append("")
        output.append("> add comment")
        output.append("")
        if self.issues[index - 1].fields.assignee:
            output.append("assigned to: " +
                          self.issues[index - 1].fields.assignee.displayName)
        else:
            output.append("> assign to me")

        # if self.issues[index - 1].fields.status.id == str(3):  # WIP
        #     output.append(">>in review")
        # else:
        #     output.append(">>start progress")
        output.append("")
        output.append('< back')
        index, key = self.r.select(ticket_number, output, width=100)
        if index in [-1, len(output) - 1]:
            self.show(user)
            return

        # if index == len(output) - 2:  # move issue to 'In Review'
        #     self.log("[status]"+self.issues[inputIndex - 1].fields.status.name)
        #     self.log("[transitions]")
        #     self.log(self.auth.transitions(ticket_number))
        #     if self.issues[inputIndex - 1].fields.status.id == str(3):  # WIP
        #         for trans in self.auth.transitions(ticket_number):
        #             if trans['name'] == "in Review":
        #                 self.log("move to 'in Review'")
        #                 self.auth.transition_issue(ticket_number, trans['id'])
        #
        #     else:
        #         for trans in self.auth.transitions(ticket_number):
        #             if trans['name'] == "Start Progress":
        #                 self.log("move to 'Start Progress'")
        #                 self.auth.transition_issue(ticket_number, trans['id'])
        #     self.show_details(inputIndex, user)
        #     return

        if index == len(output) - 4:  # add comment
            self.log("[addComment]")
            self.addComment(ticket_number)
            self.show_details(inputIndex, user)
            return

        if index == len(output) - 3:  # assign to me
            self.log("[assign to me]")
            self.auth.assign_issue(ticket_number, self.config['JIRA']['user'])
            self.show_details(inputIndex, user)
            return

        if index == 2:
            pyperclip.copy(branch_name)
            return

        # if index in [3, 4]:
        #     Popen(['notify-send', issue_description, '-t', '30000'])
        #     self.show_details(inputIndex, user)
        #     return

        # show in browser
        self.log("[show in browser]")
        uri = self.auth.issue(ticket_number).permalink()
        Popen(['nohup', self.config['JIRA']['browser'], uri],
              stdout=DEVNULL,
              stderr=DEVNULL)
예제 #3
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
예제 #4
0
class Util(object):
    """

    """
    def __init__(self, **kwargs):
        """

        """

        if 'config' in kwargs:
            self._config = kwargs['config']
        else:
            logging.critical("config was not defined")
            raise Exception("config was not defined")

        if 'username' in kwargs:
            self._username = kwargs['username']
        else:
            logging.critical("username was not defined")
            raise Exception("username was not defined")

        if 'password' in kwargs:
            self._password = kwargs['password']
        else:
            logging.critical("password was not defined")
            raise Exception("password was not defined")

        if 'project' in kwargs:
            self._project = kwargs['project']
        else:
            if 'project' in self._config:
                self._project = self._config['project']
                logging.info(
                    "project was set to '%s' from the configuration file" %
                    self._project)
            else:
                self._project = DEFAULT_PROJECT
                logging.info("project was set to default '%s'" % self._project)

        if 'base_url' in kwargs:
            self._base_url = kwargs['base_url']
        else:
            if 'base_url' in self._config:
                self._base_url = self._config['base_url']
                logging.info(
                    "base_url was set to '%s' from the configuration file" %
                    self._base_url)
            else:
                self._base_url = DEFAULT_BASE_URL
                logging.info("base_url was set to default '%s'" %
                             self._base_url)

        if 'add_missing_watchers' in kwargs:
            self._add_missing_watchers = kwargs['add_missing_watchers']
        else:
            if 'add_missing_watchers' in self._config:
                self._add_missing_watchers = self._config[
                    'add_missing_watchers']
                logging.info(
                    "add_missing_watchers was set to '%s' from the configuration file"
                    % self._add_missing_watchers)
            else:
                self._add_missing_watchers = DEFAULT_ADD_MISSING_WATCHERS
                logging.info("add_missing_watchers was set to default '%s'" %
                             self._add_missing_watchers)

        self._jira = None
        self._jra = None

        self._initialize()

    def setProject(self, project):
        """

        :param project:
        :return:
        """
        self._project = project

    def setAddMissingWatchers(self, add_missing_watchers):
        """

        :param add_missing_watchers:
        :return:
        """
        self._add_missing_watchers = add_missing_watchers

    def _initialize(self):
        """

        :return:
        """
        print("Attempting to connect to JIRA at '%s'" % self._base_url)
        self._jira = JIRA(self._base_url,
                          basic_auth=(self._username, self._password))

        print("Attempting to retrieve info for project '%s'" % self._project)
        self._jra = self._jira.project(self._project)

    def getReport(self):
        """

        :return:
        """
        self.report_misc()
        self.report_components()
        self.report_roles()
        self.report_versions()
        self.report_open_issues()

    def report_misc(self):
        """

        :return:
        """
        print(Fore.BLUE + "Project name '%s'" % self._jra.name)

        print(Fore.BLUE + "Project lead '%s'" % self._jra.lead.displayName)

        print(Style.RESET_ALL)

    def report_components(self):
        """

        :return:
        """
        components = self._jira.project_components(self._jra)

        if len(components) > 0:

            print(Fore.BLUE + "Here are the components")

            print(Style.RESET_ALL)

            for c in components:
                print(c.name)
        else:
            print(Fore.RED + "There are no components")

        print(Style.RESET_ALL)

    def report_roles(self):
        """

        :return:
        """
        roles = self._jira.project_roles(self._jra)

        if len(roles) > 0:

            print(Fore.BLUE + "Here are the roles")

            print(Style.RESET_ALL)

            for r in roles:
                print(r)
        else:
            print(Fore.RED + "There are no roles")

        print(Style.RESET_ALL)

    def report_versions(self):
        """

        :return:
        """
        versions = self._jira.project_versions(self._jra)

        if len(versions) > 0:

            print(Fore.BLUE + "Here are the versions")

            print(Style.RESET_ALL)

            for v in reversed(versions):
                print(v.name)
        else:
            print(Fore.RED + "There are no versions")

        print(Style.RESET_ALL)

    def report_watchers(self, issue):
        """

        :param issue:
        :return:
        """

        watcher = self._jira.watchers(issue)

        print("Issue '%s' has '%d' watcher(s)" %
              (issue.key, watcher.watchCount))

        current_watchers_email = {}

        for watcher in watcher.watchers:
            current_watchers_email[watcher.emailAddress] = True
            print("'%s' - '%s'" % (watcher, watcher.emailAddress))
            # watcher is instance of jira.resources.User:
            # print(watcher.emailAddress)

        for watcher_email in self._config['members_email_lookup']:
            if not watcher_email in current_watchers_email:
                print(Fore.RED +
                      "member '%s' needs to be added as a watcher to '%s'" %
                      (watcher_email, issue.key))
                username = self._config['members_email_lookup'][watcher_email]
                print("Need to add username '%s'" % username)
                print(Style.RESET_ALL)
                if self._add_missing_watchers:
                    self._jira.add_watcher(issue, username)
                    print("Exiting")
                    sys.exit(0)

        print(Style.RESET_ALL)

    def checkWatchers(self):
        """

        :return:
        """

        issues = self._jira.search_issues('project= LO AND status !=  Done',
                                          maxResults=DEFAULT_MAX_RESULTS)

        if len(issues) > 0:

            for issue in issues:
                self.report_watchers(issue)

    def report_open_issues(self):

        issues = self._jira.search_issues('project= LO AND status !=  Done',
                                          maxResults=DEFAULT_MAX_RESULTS)

        if len(issues) > 0:
            print(Fore.BLUE +
                  "Found the following '%d' open issues" % len(issues))

            print(Style.RESET_ALL)

            for issue in issues:
                summary = issue.fields.summary
                id = issue.id
                key = issue.key
                print("id '%s' key '%s' summary : '%s'" % (id, key, summary))
                if DEFAULT_REPORT_WATCHERS:
                    self._report_watchers(issue)

        print(Style.RESET_ALL)

    def getComments(self, key):
        """

        :param key:
        :return:
        """
        logging.info("Attempting to retrieve the issue with key '%s'" % key)

        issues = self._jira.search_issues('key = ' + key)
        if len(issues) > 1:
            raise Exception("Expected only one issue for '%s' but found '%d'" %
                            (key, len(issues)))

        if len(issues) == 1:
            # comments = issues[0].fields.comment.comments
            # comments = issues[0].raw['fields']['comment']['comments']
            comments = self._jira.comments(issues[0])

            if len(comments) > 0:
                print("Found the following '%d' comments" % len(comments))
                comment_ctr = 0
                for comment_id in comments:
                    print("-----------------------------------")
                    comment_ctr += 1
                    comment = self._jira.comment(key, comment_id)
                    author = comment.author.displayName
                    date_created = comment.created
                    body = comment.body
                    print(Fore.BLUE + "%d. author '%s' date '%s'" %
                          (comment_ctr, author, date_created))
                    print(Style.RESET_ALL)
                    print(body)
예제 #5
0
class jiramenu():
    user = None
    auth = None
    config = None
    debug = False
    r = Rofi()
    issues = []
    rofi_list = []

    def __init__(self, config, debug):
        self.config = config
        self.r.status("starting jiramenu")
        try:
            self.auth = JIRA(config['JIRA']['url'],
                             basic_auth=(config['JIRA']['user'],
                                         config['JIRA']['password']))
        except Exception as error:
            self.r.exit_with_error(str(error))
        self.debug = debug

    def log(self, text):
        if not self.debug:
            return
        print(text)

    def show(self, user):
        self.user = user
        if user:
            self.log(f"show issues for: {self.user}")

        query = self.config['JIRA']['query']
        if user:
            query += f" and assignee = {user}"
        self.log(f"Query: {query}")
        if not self.issues:
            self.issues = self.auth.search_issues(query)

        if not self.rofi_list:
            if user:
                self.rofi_list.append(">>ALL")
            else:
                self.rofi_list.append(">>MINE")
            for issue in self.issues:
                issuetext = ''
                if issue.fields.assignee:
                    issuetext = f'[{issue.fields.assignee.name}]'
                if issue.fields.status.id == str(3):  #id:3 = Work in Progress
                    issuetext += '{WIP}'
                issuetext += f'{issue.key}:{issue.fields.summary}'
                self.rofi_list.append(issuetext)

        # print active query plus number of results on top
        index, key = self.r.select(f'{query}[{len(self.rofi_list)}]',
                                   self.rofi_list,
                                   rofi_args=['-i'],
                                   width=100)
        del key
        if index < 0:
            exit(1)
        if index == 0:
            self.issues = []
            self.rofi_list = []
            if user:
                self.show(None)
            else:
                self.show(self.config['JIRA']['user'])
            return
        self.show_details(index, user)

    def addComment(self, ticket_number):
        comment = self.r.text_entry("Content of the comment:")
        if comment:
            # replace @user with [~user]
            comment = re.sub(r"@(\w+)", r"[~\1]", comment)
            self.auth.add_comment(ticket_number, comment)

    def show_details(self, index, user):
        inputIndex = index
        ticket_number = re.sub(r"\[.*\]", "", self.rofi_list[index])
        ticket_number = re.sub(r"\{.*\}", "", ticket_number)
        ticket_number = ticket_number.split(":")[0]
        self.log("[details]" + ticket_number)
        issue_description = self.issues[index - 1].fields.description

        output = []
        output.append(">>show in browser")
        output.append("[[status]]")
        output.append(self.issues[index - 1].fields.status.name)
        output.append("[[description]]")
        output.append(issue_description)

        if self.auth.comments(ticket_number):
            output.append("[[comments]]")
            comment_ids = self.auth.comments(ticket_number)
            for comment_id in comment_ids:
                self.log("comment_id: " + str(comment_id))
                commenttext = '[' + self.auth.comment(
                    ticket_number, comment_id).author.name + ']'
                commenttext += self.auth.comment(ticket_number,
                                                 comment_id).body
                output.append(commenttext)
        else:
            output.append("[[no comments]]")
        output.append(">>add comment")
        if self.issues[index - 1].fields.assignee:
            output.append("[[assignee]]" +
                          self.issues[index - 1].fields.assignee.name)
        else:
            output.append(">>assign to me")

        if self.issues[index - 1].fields.status.id == str(3):  # WIP
            output.append(">>in review")
        else:
            output.append(">>start progress")

        output.append('<<back')
        index, key = self.r.select(ticket_number, output, width=100)
        if index in [-1, len(output) - 1]:
            self.show(user)
            return

        if index == len(output) - 2:  # move issue to 'In Review'
            self.log("[status]" +
                     self.issues[inputIndex - 1].fields.status.name)
            self.log("[transitions]")
            self.log(self.auth.transitions(ticket_number))
            if self.issues[inputIndex - 1].fields.status.id == str(3):  # WIP
                for trans in self.auth.transitions(ticket_number):
                    if trans['name'] == "in Review":
                        self.log("move to 'in Review'")
                        self.auth.transition_issue(ticket_number, trans['id'])

            else:
                for trans in self.auth.transitions(ticket_number):
                    if trans['name'] == "Start Progress":
                        self.log("move to 'Start Progress'")
                        self.auth.transition_issue(ticket_number, trans['id'])
            self.show_details(inputIndex, user)
            return

        if index == len(output) - 4:  # add comment
            self.log("[addComment]")
            self.addComment(ticket_number)
            self.show_details(inputIndex, user)
            return

        if index == len(output) - 3:  # assign to me
            self.log("[assign to me]")
            self.auth.assign_issue(ticket_number, self.config['JIRA']['user'])
            self.show_details(inputIndex, user)
            return

        if index in [3, 4]:
            Popen(['notify-send', issue_description, '-t', '30000'])
            self.show_details(inputIndex, user)
            return

        # show in browser
        self.log("[show in browser]")
        uri = self.auth.issue(ticket_number).permalink()
        Popen(['nohup', self.config['JIRA']['browser'], uri],
              stdout=DEVNULL,
              stderr=DEVNULL)
예제 #6
0
issues = jira.search_issues('project = 10000 AND assignee!=currentUser()',
                            maxResults=0)

issue = jira.issue('MYP-6')
summary = issue.fields.summary
issue.fields.worklog.worklogs
issue.fields.worklog.worklogs[0].author
issue.fields.worklog.worklogs[0].timeSpent
issue.fields.worklog.worklogs[0].updated

jira.assign_issue(issue, 'Anakin')
transitions = jira.transitions(issue)
transition_serie = [(t['id'], t['name']) for t in transitions]

comments_b = jira.comments(issue)
print(comments_b)
comment = jira.comment('MYP-6', '10300')
print(comment.body)

issue_dict = {
    'project': {
        'id': 10000
    },
    'summary': 'New issue from jira-python',
    'description': 'Look into this one',
    'issuetype': {
        'name': 'Snake'
    },
}
new_issue = jira.create_issue(fields=issue_dict)