def updateIssues(issuelist, NEXTorDOTX, description):
	numExistingIssues = len(issuelist) if not issuelist == None else 0
	if numExistingIssues > 0 : 
		if debug: print "[DEBUG] Move " + str(numExistingIssues) + " " + description
		jira = JIRA(options={'server':jiraserver}, basic_auth=(jirauser, jirapwd))

		cnt = 0
		for s in issuelist :
			key = components.getText(components.findChildNodeByName(s, 'key').childNodes)
			issue = jira.issue(key)
			cnt += 1
			doThisJIRA = True
			whichLabelSkipped = ""
			for label in issue.fields.labels:
				for skipLabel in skipLabels:
					if label == skipLabel.strip():
						whichLabelSkipped = label
						doThisJIRA = False

			linkURL = components.getText(components.findChildNodeByName(s, 'link').childNodes)
			summary = components.getText(components.findChildNodeByName(s, 'summary').childNodes).strip()
			operation = " + [" + str(cnt) + "/" + str(len(issuelist)) + "] Update " + linkURL + " : " + summary
			if debug: operation = operation + " :: " + str(issue.fields.labels)

			if doThisJIRA == False:
				operation = " - [" + str(cnt) + "/" + str(len(issuelist)) + "] -Skip- " + linkURL + " (" + whichLabelSkipped + ") : " + summary
				print operation
			else:
				if options.autoApplyChanges or options.dryrun: 
					print operation
					yesno = ""
				else:
					yesno = raw_input(operation + " ? [y/N] ")
				if options.autoApplyChanges or yesno.capitalize() in ["Y"]:
					# move issue to next fixversion
					if components.findChildNodeByName(s, 'project').attributes["key"].value == "JBIDE": # JBIDE or JBDS
						fixversion = version_jbt
						fixversion_NEXT = version_jbt_NEXT if NEXTorDOTX else version_jbt_DOTX
					else:
						fixversion = version_ds
						fixversion_NEXT = version_ds_NEXT if NEXTorDOTX else version_ds_DOTX

					fixVersions = []
					# NOTE: if there is more than one fixversion, the others will not be changed
					for version in issue.fields.fixVersions:
						if version.name != fixversion:
							fixVersions.append({'name': version.name})
					fixVersions.append({'name': fixversion_NEXT})
					issue.update(fields={'fixVersions': fixVersions})

					# only for NEXT, not for .x
					if NEXTorDOTX:
						# move issue to new sprint
						jira.add_issues_to_sprint(sprintId_NEXT, [key])
						jira.add_comment(key, "[checkUnresolvedIssues.py] Slip to fixversion = *" + fixversion_NEXT + "* and sprint = *" + sprint_NEXT + "*")
					else:
						jira.add_comment(key, "[checkUnresolvedIssues.py] Slip to fixversion = *" + fixversion_NEXT + "*")
Example #2
0
def updateIssues(issuelist, NEXTorDOTX, description):
	numExistingIssues = len(issuelist) if not issuelist == None else 0
	if numExistingIssues > 0 : 
		if debug: print "[DEBUG] Move " + str(numExistingIssues) + " " + description
		jira = JIRA(options={'server':jiraserver}, basic_auth=(jirauser, jirapwd))

		cnt = 0
		for s in issuelist :
			key = components.getText(components.findChildNodeByName(s, 'key').childNodes)
			issue = jira.issue(key)
			cnt += 1
			doThisJIRA = True
			whichLabelSkipped = ""
			for label in issue.fields.labels:
				for skipLabel in skipLabels:
					if label == skipLabel.strip():
						whichLabelSkipped = label
						doThisJIRA = False

			linkURL = components.getText(components.findChildNodeByName(s, 'link').childNodes)
			summary = components.getText(components.findChildNodeByName(s, 'summary').childNodes).strip()
			operation = " + [" + str(cnt) + "/" + str(len(issuelist)) + "] Update " + linkURL + " : " + summary
			if debug: operation = operation + " :: " + str(issue.fields.labels)

			if doThisJIRA == False:
				operation = " - [" + str(cnt) + "/" + str(len(issuelist)) + "] -Skip- " + linkURL + " (" + whichLabelSkipped + ") : " + summary
				print operation
			else:
				if options.autoApplyChanges or options.dryrun: 
					print operation
					yesno = ""
				else:
					yesno = raw_input(operation + " ? [y/N] ")
				if options.autoApplyChanges or yesno.capitalize() in ["Y"]:
					# move issue to next fixversion
					if components.findChildNodeByName(s, 'project').attributes["key"].value == "JBIDE": # JBIDE or JBDS
						fixversion = version_jbt
						fixversion_NEXT = version_jbt_NEXT if NEXTorDOTX else version_jbt_DOTX
					else:
						fixversion = version_ds
						fixversion_NEXT = version_ds_NEXT if NEXTorDOTX else version_ds_DOTX

					fixVersions = []
					# NOTE: if there is more than one fixversion, the others will not be changed
					for version in issue.fields.fixVersions:
						if version.name != fixversion:
							fixVersions.append({'name': version.name})
					fixVersions.append({'name': fixversion_NEXT})
					issue.update(fields={'fixVersions': fixVersions})

					# only for NEXT, not for .x
					if NEXTorDOTX:
						# move issue to new sprint
						jira.add_issues_to_sprint(sprintId_NEXT, [key])
						jira.add_comment(key, "[checkUnresolvedIssues.py] Slip to fixversion = " + fixversion_NEXT + " and sprint " + sprintId_NEXT)
					else:
						jira.add_comment(key, "[checkUnresolvedIssues.py] Slip to fixversion = " + fixversion_NEXT)
Example #3
0
def set_jira_rtbf(jira_rtbf_issue, jira_usr, jira_pwd, result):
    from jira import JIRA
    from jira.resources import Issue
    jira = JIRA(basic_auth=(jira_usr, jira_pwd), options={'server':'https://jira.smc.com'})
    issue = jira.issue(jira_rtbf_issue)
    jira.add_comment(jira_rtbf_issue, result)
    issue.update(assignee={'name': (issue.fields.reporter.name)})
    issues_lst=[]
    issues_lst.append(issue.key)
    #spint_id 3844 is for 'COMPLETED GDPR Requests'
    jira.add_issues_to_sprint(3844,issues_lst)
    print('success')
Example #4
0
class JIRAUtilities:
    # Translate state changes to JIRA transition values
    transition_ids = {'ToDo': '11', 'Prog': '21', 'Imped': '41', 'Done': '31'}

    user_story_dict_cloud = {
        #'project': 'SP',
        #'project': 'TTP',  # Tsafi, 15 Jan 2020
        'project':
        DEFAULT_JIRA_PARAMS[JIRA_INST]['PROJECT'],  # Tsafi 3 Feb 2020
        'issuetype': 'Story',
        'summary': "Story 302",
        'customfield_10322': [{
            'value': 'Dev Team 1'
        }]  # Team field ********cloud
    }

    user_story_dict_vm = {
        #'project': 'SP',
        #'project': 'TTP', #Tsafi 14 Jan 2020 - temp change to work with TTP project already exists on local Jira
        'project':
        DEFAULT_JIRA_PARAMS[JIRA_INST]['PROJECT'],  # Tsafi 3 Feb 2020
        'issuetype': 'Story',
        'summary': "Story 302",
        #'customfield_10201': [{'value': 'Dev Team 1'}]  # Team field vm
        'customfield_10200': [{
            'value': 'Dev Team 1'
        }]  # Tsafi 21 Jan 2020
    }

    # 10120 is the 'Epic Name' and it's a must have
    epic_dict_cloud = {
        #'project': 'SP',
        #'project': 'TTP',  # Tsafi, 15 Jan 2020
        'project':
        DEFAULT_JIRA_PARAMS[JIRA_INST]['PROJECT'],  # Tsafi 3 Feb 2020
        #'issuetype': 'Epic',
        'issuetype': {
            'name': 'Epic'
        },  # Tsafi 15 Jan 2020
        'summary': "Epic 1",
        'customfield_10120':
        "Epic 1",  # Epic name is the same as Epic summary ********cloud
        'customfield_10322': [{
            'value': 'Dev Team 1'
        }]  # Team field ********cloud
    }

    epic_dict_vm = {
        'project':
        DEFAULT_JIRA_PARAMS[JIRA_INST]['PROJECT'],  # Tsafi 3 Feb 2020
        'issuetype': 'Epic',
        'summary': "Epic 1",
        'customfield_10103':
        "Epic 1",  # Epic name is the same as Epic summary vm
        #'customfield_10102': "Epic 1",  # Tsafi 14 Jan 2020
        'customfield_10200': [{
            'value': 'Dev Team 1'
        }]  # Tsafi 21 Jan 2020 - Team field vm
    }

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

        if instance_type == 'cloud':
            # Using Nela's Jira cloud account
            #self.jira_inst = JIRA(basic_auth=('*****@*****.**', 'FiJBeI3H81sceRofBcY4E84E'),
            #                      options={'server': 'https://dr-agile.atlassian.net'})
            # Using Tsafi's Jira cloud account
            #self.jira_inst = JIRA(basic_auth=('*****@*****.**', 'tDshA7M9zEhMkaiC13RA146E'),
            #                      options={'server': 'https://dr-agile.atlassian.net'})
            self.jira_inst = JIRA(
                basic_auth=(DEFAULT_JIRA_PARAMS['CLOUD']['USER'],
                            DEFAULT_JIRA_PARAMS['CLOUD']['PASS']),
                options={'server': DEFAULT_JIRA_PARAMS['CLOUD']['URL']})
        else:
            # Using Nela's Jira local VM
            #self.jira_inst = JIRA(basic_auth=('nela.g', 'q1w2e3r4'), options={'server': 'http://192.168.56.101:8080'})
            # Using Tsafi's Jira local VM
            #self.jira_inst = JIRA(basic_auth=('tsafrir.m', 'Sim1965'), options={'server': 'http://192.168.43.55:8080'})
            #self.jira_inst = JIRA(basic_auth=('tsafrir.m', 'Sim1965'), options={'server': 'http://192.168.43.41:8080'})
            #self.jira_inst = JIRA(basic_auth=('tsafrir.m', 'Sim1965'), options={'server': 'http://10.0.0.61:8080'})
            self.jira_inst = JIRA(
                basic_auth=(DEFAULT_JIRA_PARAMS['LOCAL']['USER'],
                            DEFAULT_JIRA_PARAMS['LOCAL']['PASS']),
                options={'server': DEFAULT_JIRA_PARAMS['LOCAL']['URL']})

    def __del__(self):
        del self.jira_inst

    # Creation
    def create_epic(self, name, team):
        if self.instance_type == 'cloud':
            dict = self.epic_dict_cloud
            dict['customfield_10120'] = name  # ********cloud
            dict['customfield_10322'] = [{'value': team.name}]  # ********cloud
        else:
            dict = self.epic_dict_vm
            dict['customfield_10103'] = name  # tsafi 21 Jan 2020
            #dict['customfield_10102'] = name # Tsafi 14 Jan 2020
            #dict['customfield_10201'] = {'value': team.name}
            dict['customfield_10200'] = {
                'value': team.name
            }  # Tsafi 21 Jan 2020

        dict['summary'] = name

        epic = self.jira_inst.create_issue(fields=dict)
        return epic

    def create_user_story_with_epic(self,
                                    user_story_name,
                                    team,
                                    epic_key=None):
        if self.instance_type == 'cloud':
            dict = self.user_story_dict_cloud
            dict['customfield_10322'] = [{'value': team.name}]  # ********cloud
        else:
            dict = self.user_story_dict_vm
            #dict['customfield_10201'] = {'value': team.name}
            dict['customfield_10200'] = {
                'value': team.name
            }  # Tsafi 21 Jan 2020

        dict['summary'] = user_story_name

        user_story = self.jira_inst.create_issue(fields=dict)

        if epic_key:
            self.jira_inst.add_issues_to_epic(epic_key, [user_story.key])

        # Note the performance impact, this is another fetch after
        # adding a story to epic
        user_story = self.jira_inst.issue(user_story.id)
        return user_story

    def create_list_of_epics(self, list_of_epics, team):
        print('\nCreating list of epics for team %s in JIRA' % team.name)
        for epic in list_of_epics:
            jira_epic = self.create_epic(epic.name, team)
            epic.key = jira_epic.key

    def create_list_of_user_stories(self, list_of_user_stories, team):
        print('\nCreating list of user stories for team %s in JIRA' %
              team.name)
        for user_story in list_of_user_stories:
            jira_user_story = self.create_user_story_with_epic(
                user_story.name, team, user_story.epic.key)
            user_story.key = jira_user_story.key

    # Create Sprint
    # Best would be to provide a board_id of a board that contains all issues
    def create_sprint(self, board_id, sprint_name, start_date, sprint_size):
        end_date = start_date + timedelta(days=sprint_size)

        start_date_str = start_date.strftime("%d/%b/%y %#I:%M %p")
        end_date_str = end_date.strftime("%d/%b/%y %#I:%M %p")
        print(start_date_str)
        print(end_date_str)

        new_sprint = self.jira_inst.create_sprint(sprint_name,
                                                  board_id,
                                                  startDate=start_date_str,
                                                  endDate=end_date_str)

        print("Created new Sprint: name = %s, id =  %s" %
              (new_sprint.name, new_sprint.id))
        print("Start date %s, end date %s" %
              (new_sprint.startDate, new_sprint.endDate))

        return new_sprint

    # Set up Sprints
    def start_sprint(self, sprint_id):
        print("Please start the sprint manually on a global board")
        key = input()

    def end_sprint(self, sprint_id):
        print("Please end the sprint manually on a global board")
        key = input()

    def add_issues_to_sprint(self, sprint_id, user_stories_keys):
        print("Adding %d issues to Sprint %d" %
              (len(user_stories_keys), sprint_id))
        self.jira_inst.add_issues_to_sprint(sprint_id, user_stories_keys)

    def update_one_issue(self, issue_key, transition):
        print("Updating issue %s, moving to %s" % (issue_key, transition))
        self.jira_inst.transition_issue(issue_key,
                                        self.transition_ids[transition])

    def advance_time_by_one_day(self):
        if self.instance_type != 'cloud':
            #Tsafi 20 Jan 2020, change path for Tsafi's local VM
            #path = "C:\\Windows\\WinSxS\\amd64_openssh-client-components-onecore_31bf3856ad364e35_10.0.17763.1_none_f0c3262e74c7e35c\\ssh.exe [email protected] \"cd /home/nelkag/Simulator/misc; python /home/nelkag/Simulator/misc/advanceoneday.py\""
            #path = "C:\\Windows\\System32\\OpenSSH\\ssh.exe [email protected] \"cd /home/tsafi/SAFESimulator; python3 ./advanceoneday.py\""
            #path = "C:\\Windows\\System32\\OpenSSH\\ssh.exe [email protected] python3 /home/tsafi/SAFESimulator/advanceoneday.py"
            #path = "C:\\Windows\\System32\\OpenSSH\\ssh.exe [email protected] date"
            #path = "C:\\Windows\\Sysnative\\OpenSSH\\ssh.exe [email protected] python3 ./SAFESimulator/advanceoneday.py"
            #path = "C:\\Windows\\Sysnative\\OpenSSH\\ssh.exe [email protected] python3 ./SAFESimulator/advanceoneday.py"
            path = 'C:\\Windows\\Sysnative\\OpenSSH\\ssh.exe tsafi@' + LOCAL_JIRA_IP + ' python3 ./SAFESimulator/advanceoneday.py'
            print(path)
            os.system(path)
Example #5
0
def main():

    parser = argparse.ArgumentParser(description='Creates TestRail Test Runs and Jira tasks for test execution')
    parser.add_argument('--filter', dest='filter', action='store_false',
                        help='will also add test cases which have been automated')
    args = parser.parse_args()

    user_details = {
        'id': 'ATLASSIAN ID HERE',
        'email': 'EMAIL HERE',
        'jiraKey': 'ATLASSIAN KEY HERE',
        'testrailKey': 'TESTRAIL KEY HERE'
    }

    ssl._create_default_https_context = ssl._create_unverified_context

    # Provide authentication details for accessing TestRail's API
    client = APIClient('https://snapsheet.testrail.io/')
    client.user = user_details['email']
    client.password = user_details['testrailKey']


    # Dictionary to hold relevant IDs from QA Regression project
    id_dict = {1: {'suite_id': 64,
                   'project': 'S2'
                   },
               2: {'suite_id': 112,
                   'project': 'VICE'
                   },
               3: {'suite_id': 92,
                   'project': 'SnapTx'
                   },
               4: {'suite_id': 67,
                   'project': 'Shops'
                   },
               5: {'suite_id': 66,
                   'project': 'Hertz'
                   },
               6: {'suite_id': 68,
                   'project': 'S1'
                   },
               7: {'suite_id': 78,
                   'project': 'Turo'
                   },
               8: {'suite_id': 69,
                   'project': 'Policy App'
                   }
               }

    print(
          '\n[1] Standard 2\n'
          '[2] VICE\n'
          '[3] SnapTx\n'
          '[4] Shops\n'
          '[5] Hertz\n'
          '[6] Standard 1\n'
          '[7] Turo\n'
          '[8] Policy',
          )

    userInput = int(input('\nEnter the key (1 - 8) corresponding to the product for which you\'d like to create a new run: '))

    while userInput not in range(len(id_dict) + 1) or userInput == 0:
        try:
            userInput = int(input())
        except:
            print('That\'s not a number...')
            sys.exit(1)

    caseIdArray = []
    testCases = client.send_get("get_cases/10&suite_id=%s" % (str(getSuiteId(userInput, id_dict))))
    for index in range(len(testCases)):
        if testCases[index]['custom_test_case_automated'] is not True:
            caseIdArray.append(testCases[index]['id'])

    buildName = input('\nPlease enter a name for the new build: ')

    if args.filter is not False:
        newRun = client.send_post('add_run/10', {
            'suite_id': getSuiteId(userInput, id_dict),
            'name': buildName,
            'include_all': False,
            'case_ids': caseIdArray
        })
    else:
        newRun = client.send_post('add_run/10', {
            'suite_id': getSuiteId(userInput, id_dict),
            'name': buildName,
            'include_all': True
        })

    # Provide authentication details for Jira
    options = {'server': 'https://snapsheettech.atlassian.net/'}
    jira = JIRA(options, basic_auth=(user_details['email'], user_details['jiraKey']))

    createTask = input('\nDo you want to create a Jira task for this test run? (Yes/No): ')

    while createTask.lower() not in ['y','n','yes','no']:
        try:
            createTask = input('Try again...')
        except:
            pass

    if createTask.lower() in ['y', 'yes']:
        task = jira.create_issue(project='QA', summary='%s - Regression Run' % buildName, issuetype={'name': 'Task'},
                                 description=newRun['url'], assignee={'id': user_details['id']})

        addToSprint = input('\nDo you want to add the task to the current sprint? (Yes/No): ')

        while addToSprint.lower() not in ['y', 'n', 'yes', 'no']:
            try:
                addToSprint = input('Try again...')
            except:
                pass

        if addToSprint.lower() in ['y', 'yes']:
            sprints = jira.sprints(30, maxResults=None)
            jira.add_issues_to_sprint(sprints[len(sprints)-1].id, [task.key])
    else:
        pass

    print("\nHere\'s the URL for the new %s build: %s \n" % (id_dict[userInput]['project'], newRun['url']))
Example #6
0
def create_issues_in_jira(*, issue_dicts, server_url, basic_auth, project_name,
                          board_name, assignee_key, components, epic_link,
                          max_results):
    jira = JIRA(server=server_url, basic_auth=basic_auth)

    def get_board(board_name):
        for board in jira.boards():
            if board.name == board_name:
                return board
        return None

    def get_project(project_name):
        for project in jira.projects():
            if project.name == project_name:
                return project
        return None

    project = get_project(project_name)
    if not project:
        raise Exception('project not found: \'{}\''.format(board_name))

    board = get_board(board_name)
    if not board:
        raise Exception('board not found: \'{}\''.format(board_name))

    component_ids = None
    if components is not None:
        component_objs = jira.project_components(project)

        def get_component(component_name):
            for component_obj in component_objs:
                if component_obj.name == component_name:
                    return component_obj
            return None

        component_ids = []
        for component in components:
            component_obj = get_component(component)
            if component_obj is None:
                raise Exception(
                    'component not found: \'{}\''.format(component))
            component_ids.append(component_obj.id)

    if epic_link:
        search_result = jira.search_issues(
            'summary ~ "{}"'.format(f'\\"{epic_link}\\"'))
        if len(search_result) == 0:
            raise Exception('epic not found: \'{}\''.format(epic_link))
        elif len(search_result) > 1:
            raise Exception(
                'more than one epic found for name \'{}\''.format(epic_link))
        epic = search_result[0]
    else:
        epic = None

    create_issues_results = []

    def _create_issue(issue_dict, parent=None):
        fields_list = [{
            'project': {
                'key': project.key
            },
            'summary': issue_dict['summary'],
            'description': issue_dict['description'],
            'issuetype': {
                'name': 'Task' if parent is None else 'Sub Task'
            },
        }]
        if parent is None:
            fields_list[0]['assignee'] = {
                'name':
                issue_dict['assignee']
                if issue_dict['assignee'] else assignee_key
            }
        if component_ids is not None:
            fields_list[0]['components'] = [{
                'id': component_id
            } for component_id in component_ids]
        if parent is not None:
            fields_list[0]['parent'] = {'key': parent.key}
        results = jira.create_issues(fields_list)
        assert len(results) == 1
        result = results[0]
        issue_obj = result['issue']
        for sub_issue_dict in issue_dict['sub_issues']:
            _create_issue(sub_issue_dict, issue_obj)
        create_issues_results.append(
            dict(issue_dict=issue_dict, issue_obj=issue_obj))

    for issue_dict in issue_dicts:
        _create_issue(issue_dict)

    if epic is not None:
        jira.add_issues_to_epic(epic.id, [
            create_issues_result['issue_obj'].key
            for create_issues_result in create_issues_results if
            create_issues_result['issue_obj'].fields.issuetype.name == 'Task'
        ])

    issues_to_add_to_sprint = [
        create_issues_result['issue_obj'].key
        for create_issues_result in create_issues_results
        if create_issues_result['issue_dict'].get('add_to_sprint', False)
    ]
    if issues_to_add_to_sprint:
        sprints = jira.sprints(board.id,
                               extended=['startDate', 'endDate'],
                               maxResults=max_results)
        if len(sprints) == 0:
            raise Exception('There\'s no open sprint')
        last_sprint = sprints[-1]
        jira.add_issues_to_sprint(last_sprint.id, issues_to_add_to_sprint)
Example #7
0
class JiraTool:
    def __init__(self):
        self.server = jira_url
        self.basic_auth = (usrer, password)
        self.jiraClinet = None

    def login(self):
        self.jiraClinet = JIRA(server=self.server, basic_auth=self.basic_auth)
        if self.jiraClinet != None:
            return True
        else:
            return False

    def findIssueById(self, issueId):
        if issueId:
            if self.jiraClinet == None:
                self.login()
            return self.jiraClinet.issue(issueId)
        else:
            return 'Please input your issueId'

    def deleteAllIssue(self, project):
        project_issues = self.jiraClinet.search_issues('project=' +
                                                       project.name)
        for issue in project_issues:
            logging.info('delete issue %s' % (issue))
            issue.delete()

    def deleteAllSprint(self, board):
        sprints = self.jiraClinet.sprints(board.id)
        for sprint in sprints:
            logging.info('delete sprint %s' % (sprint.name))
            sprint.delete()

    def getProject(self, name):
        projects = self.jiraClinet.projects()
        #logging.info("get project %s" %(name))
        for project in projects:
            #logging.info("project %s" %(project.name))
            if (name == project.name):
                return project

        return None
        #return self.jiraClinet.create_project(key='SCRUM', name=name, assignee='yujiawang', type="Software", template_name='Scrum')

    def getBoard(self, project, name):
        boards = self.jiraClinet.boards()
        for board in boards:
            if (name == board.name):
                logging.info("board:%s id:%d" % (board.name, board.id))
                return board

        return self.jiraClinet.create_board(name, [project.id])

    def createSprint(self, board, name, startDate=None, endDate=None):
        logging.info("==== create sprint[%s] in board[%s] ====>" %
                     (name, board.name))
        sprint = self.jiraClinet.create_sprint(name, board.id, startDate,
                                               endDate)
        return sprint

    def getSprint(self, board_id, sprint_name):
        sprints = self.jiraClinet.sprints(board_id)
        for sprint in sprints:
            if sprint.name == sprint_name:
                return sprint
        return None

    def createEpicTask(self, project, sprint, summary, description, assignee,
                       participant, duedate):
        issue_dict = {
            'project': {
                'key': project.key
            },
            'issuetype': {
                'id': issuetypes['Epic']
            },
            'customfield_10002': summary,  # epic 名称
            'summary': summary,
            'description': description,
            "customfield_10004": sprint.id,  # sprint
            'assignee': {
                'name': assignee
            },
            'customfield_10303': participant,
            'customfield_10302': '2018-08-24T05:41:00.000+0800'
        }

        logging.info(duedate)

        logging.info(issue_dict)  #juse for debug
        issue = self.jiraClinet.create_issue(issue_dict)
        self.jiraClinet.add_issues_to_sprint(sprint.id, [issue.raw['key']])
        logging.info(
            "===> add epic task[%s key:%s] to sprint [%s]" %
            (issue.raw['fields']['summary'], issue.raw['key'], sprint.name))
        #dumpIssue(issue) #juse for debug
        return issue

    def createTask(self, project, sprint, epic, summary, description, assignee,
                   participant):
        issue_dict = {
            'project': {
                'key': project.key
            },
            'issuetype': {
                'id': issuetypes['Task']
            },
            'summary': summary,
            'description': description,
            'assignee': {
                'name': assignee
            },
            'customfield_10303': participant
        }

        issue = self.jiraClinet.create_issue(issue_dict)
        logging.info("==> add task[%s key:%s] link epic [%s key: %s]" %
                     (issue.raw['fields']['summary'], issue.raw['key'],
                      epic.raw['fields']['summary'], epic.raw['key']))
        self.jiraClinet.add_issues_to_epic(epic.id, [issue.raw['key']])
        self.jiraClinet.add_issues_to_sprint(sprint.id, [issue.raw['key']])
        return issue

    def createSubTask(self, project, parent, summary, description, assignee,
                      participant):
        issue_dict = {
            'project': {
                'key': project.key
            },
            'parent': {
                'key': parent.raw['key']
            },
            'issuetype': {
                'id': issuetypes['Sub-Task']
            },
            'summary': summary,
            'description': description,
            'assignee': {
                'name': assignee
            },
            'customfield_10303': participant
        }
        issue = self.jiraClinet.create_issue(issue_dict)
        logging.info("=> add sub task[%s key:%s] to task [%s key: %s]" %
                     (issue.raw['fields']['summary'], issue.raw['key'],
                      parent.raw['fields']['summary'], parent.raw['key']))
        return issue
Example #8
0
class JiraAPI(object):
    """Access Jira api using python-jira project."""
    def __init__(self, user, passwd, logger):
        """Init JiraApi object and logger."""
        self.logger = logger
        self.jira = JIRA(server=config.JIRA_HOST, basic_auth=(user, passwd))

    def get_boards(self):
        """Get Jira boards list as json."""
        self.logger.info("Getting Jira boards")
        json = {'boards': []}
        boards = self.jira.boards()
        for board in boards:
            json.get('boards').append({'id': board.id, 'name': board.name})
        return json

    # ###############
    # ### Sprints ###
    # ###############

    def _get_board_sprints(self, board_id):
        """Get sprints of a board as json."""
        self.logger.info("Getting board {} sprints".format(board_id))
        json = {'sprints': []}
        sprints = self.jira.sprints(board_id, extended=True)
        for sprint in sprints:
            # self.logger.debug("Sprint content: {}".format(sprint.__dict__))
            json.get('sprints').append({
                'id': sprint.id,
                'key': sprint.id,
                'name': sprint.name,
                'start': sprint.startDate,
                'end': sprint.endDate
            })
        return json

    def search_sprint(self, board_key, sprint_name):
        """Search a sprint in a board and returns its info."""
        self.logger.info("Searching board {} sprint '{}'".format(
            board_key, sprint_name))
        found_sprint = None
        sprints = self._get_board_sprints(board_key)
        for sprint in sprints.get('sprints'):
            if (sprint.get('name') == sprint_name):
                found_sprint = sprint
                break
        return found_sprint

    def create_sprint(self, board_id, sprint_name, start, end):
        """Create a sprint in a board with given start and end dates.

        Dates must be in Jira format.
        """
        self.logger.info("Creating sprint {}".format(sprint_name))
        sprint = self.jira.create_sprint(name=sprint_name,
                                         board_id=board_id,
                                         startDate=start,
                                         endDate=end)
        # self.logger.debug("Created sprint content {}".format(sprint.__dict__))
        return sprint.id

    def start_sprint(self, sprint_id, sprint_name, start, end):
        """Start a Jira sprint with given dates. *** Does not work because Jira API call fails with 'state' param. ***

        Dates must be in Jira format.
        """
        self.logger.info("Starting sprint {} [Not working]".format(sprint_id))
        # self.jira.update_sprint(sprint_id, name=sprint_name, startDate=start, endDate=end, state="active")

    def close_sprint(self, sprint_id, sprint_name, start, end):
        """Closes a Jira sprint with given dates. *** Does not work because Jira API call fails with 'state' param. ***

        Dates must be in Jira format.
        """
        self.logger.info("Closing sprint {} [Not working]".format(sprint_id))
        # self.jira.update_sprint(sprint_id, name=sprint_name, startDate=start, endDate=end, state=None)

    def delete_board_sprints(self, board_id):
        """Delete all sprints of a board."""
        self.logger.info("Deleting board {} sprints".format(board_id))
        sprints = self.jira.sprints(board_id, extended=False)
        for sprint in tqdm(sprints):
            self.logger.info("Deleting sprint {}".format(sprint.name))
            sprint.delete()

    def _get_board_project_id(self, board_id):
        """Get the project id of a board."""
        self.logger.debug("Getting project id of board {}".format(board_id))
        project_id = None
        boards = self.jira.boards()
        for board in boards:
            # self.logger.debug("Comparing boards {}-{}".format(board.id, board_id))
            if str(board.id) == str(board_id):
                # self.logger.debug("Found matching board {}".format(board.raw))
                project_id = board.raw.get("filter").get("queryProjects").get(
                    "projects")[0].get("id")
                break
        return project_id

    # ####################
    # ### User stories ###
    # ####################

    def _get_project_user_stories(self, project_key):
        """Get all user stories of a project."""
        self.logger.info("Getting project {} user stories".format(project_key))
        issues = self.jira.search_issues('project=' + project_key +
                                         ' and issuetype=' +
                                         config.JIRA_USER_STORY_TYPE,
                                         maxResults=200)
        return issues

    def delete_all_project_user_stories(self, project_key):
        """Delete all user stories of a project."""
        self.logger.info(
            "Deleting project {} user stories".format(project_key))
        issues = self._get_project_user_stories(project_key)
        for issue in tqdm(issues):
            self.logger.info("Deleting user story {}".format(issue))
            issue.delete()

    def _create_user_story(self, project_id, subject, description, tags,
                           points):
        """Create a user story with provided information."""
        story_fields = {
            "project": project_id,
            "issuetype": config.JIRA_USER_STORY_TYPE,
            "summary": subject,
            "description": description,
            "labels": tags,
            config.JIRA_ESTIMATION_FIELD: points
        }
        created_story = self.jira.create_issue(fields=story_fields)
        self.logger.info("Created user story {} # {}".format(
            created_story.id, created_story.key))
        # self.logger.debug("Created user story details: {}".format(created_story.__dict__))
        return created_story

    def update_user_story_status(self, user_story_key, is_closed, status):
        """Update the status of a user story to Done or Not Done only if it's closed.

        A translation is made between the status given, which is the status in Taiga and the Jira status as
        defined in JIRA_USER_STORY_STATUS_DONE, JIRA_USER_STORY_STATUS_NOT_DONE and TAIGA_USER_STORY_STATUS_NOT_DONE
        constants of config.py file.
        """
        self.logger.info(
            "Updating user story {} with Taiga status={} ({})".format(
                user_story_key, status, is_closed))
        if is_closed:
            task_status = config.JIRA_USER_STORY_STATUS_DONE
            if status == config.TAIGA_USER_STORY_STATUS_NOT_DONE:
                task_status = config.JIRA_USER_STORY_STATUS_NOT_DONE
            self.jira.transition_issue(user_story_key, task_status)
            self.logger.info("Updated user story {} status to {}".format(
                user_story_key, task_status))
        else:
            self.logger.warn(
                "Not updated user story {} beacuse is not closed: is_closed={}"
                .format(user_story_key, is_closed))

    # ########################
    # ### User story tasks ###
    # ########################

    def _create_user_story_task(self, project_id, user_story_id, subject,
                                description, finished_date):
        """Create a task inside a user story.

        The story task type is defined in config.py in JIRA_USER_STORY_TASK_TYPE constant. default is 'Sub-type'.
        """
        self.logger.info("Creating user story task {}".format(subject))
        created_task = self.jira.create_issue(
            project=project_id,
            parent={"id": user_story_id},
            issuetype=config.JIRA_USER_STORY_TASK_TYPE,
            summary=subject)
        self.logger.info("Created user story task {} # {}".format(
            created_task.id, created_task.key))
        # self.logger.debug("Created story task details: {}".format(created_task.__dict__))
        return created_task

    def _update_task_status(self, task_key, status):
        """Update task status with Done or not Done status depending on incoming Taiga status.

        Taiga done status is defined in TAIGA_TASK_STATUS_DONE constant inside config.py file.
        Jira done and not done statuses are defined in JIRA_TASK_STATUS_DONE and JIRA_TASK_STATUS_NOT_DONE inside
        config.py file.
        """
        self.logger.info(
            "Updating user story task {} with Taiga status={}".format(
                task_key, status))
        task_status = config.JIRA_TASK_STATUS_DONE
        if status != config.TAIGA_TASK_STATUS_DONE:
            task_status = config.JIRA_TASK_STATUS_NOT_DONE
        self.jira.transition_issue(task_key, task_status)
        self.logger.info("Updated user story task {} status to {}".format(
            task_key, task_status))

    # #######################
    # ### Sprint creation ###
    # #######################

    def _add_comment(self, issue_id, comment):
        """Add a comment to an issue."""
        self.logger.debug("Adding comment to issue {}".format(issue_id))
        self.jira.add_comment(issue_id,
                              comment,
                              visibility={'group': 'jira-users'})

    def create_sprint_stories(self, board_id, sprint_id, user_stories):
        """Create user stories in a board and link them to a sprint.

        A json array with user story key, is_closed and status info is returned.
        User stories subtasks are also added to the story.
        User story tasks finished date and current taiga status are added as comments.
        User story finished date and current taiga status are added as comments.
        """
        project_id = self._get_board_project_id(board_id)
        self.logger.info(
            "Creating user stories in project {} - sprint {}".format(
                project_id, sprint_id))
        created_stories = []
        for user_story in user_stories:
            self.logger.info("Creating user story {}".format(
                user_story.get("subject")))
            created_story = self._create_user_story(
                project_id, user_story.get("subject"),
                user_story.get("description"), user_story.get("tags"),
                user_story.get("total_points"))
            self.logger.info("Adding user story {} to sprint {}".format(
                created_story.key, sprint_id))
            self.jira.add_issues_to_sprint(sprint_id, [created_story.key])
            for task in user_story.get("tasks"):
                created_task = self._create_user_story_task(
                    project_id, created_story.id, task.get("subject"),
                    task.get("description"), task.get("finished_date"))
                # Add as comment user story finished date
                self._add_comment(
                    created_task.id,
                    "Finished date: '{}'".format(task.get('finished_date')))
                self._add_comment(
                    created_task.id,
                    "Taiga status: '{}'".format(task.get("status")))
                # Update task status
                self._update_task_status(created_task.key, task.get("status"))
            created_stories.append({
                "key": created_story.key,
                "is_closed": user_story.get("is_closed"),
                "status": user_story.get("status")
            })
            # Add as comment user story finished date
            self._add_comment(
                created_story.id,
                "Finished date: '{}'".format(user_story.get('finish_date')))
            self._add_comment(
                created_story.id,
                "Taiga status: '{}'".format(user_story.get("status")))
        return created_stories

    def add_backlogs_stories(self, board_id, user_stories):
        """Add user stories to the backlog of the board.

        Taiga original status and backlog order are added as comments.
        """
        project_id = self._get_board_project_id(board_id)
        self.logger.info(
            "Creating user stories in project {} backlog".format(project_id))
        created_stories = []
        for user_story in user_stories:
            self.logger.info("Creating user story {}".format(
                user_story.get("subject")))
            created_story = self._create_user_story(
                project_id, user_story.get("subject"),
                user_story.get("description"), user_story.get("tags"),
                user_story.get("total_points"))
            created_stories.append({"key": created_story.key})
            # Add as comment user story finished date
            self._add_comment(
                created_story.id,
                "Taiga status: '{}'".format(user_story.get("status")))
            self._add_comment(
                created_story.id, "Taiga backlog order: '{}'".format(
                    user_story.get("backlog_order")))
        return created_stories
Example #9
0
class JSprint(cmd.Cmd):

    jira = None

    __current_sprint = None

    def __init__(self):
        super().__init__()

        username = settings.get("jira_username")
        password = settings.get("jira_password") or getpass.getpass()

        options = {
            "server": settings.get("jira_url"),
            "agile_rest_path": "agile"
        }

        self.jira = JIRA(options, auth=(username, password))

    @property
    def prompt(self):
        sprint = Style.BRIGHT + self.current_sprint.name + Style.RESET_ALL

        return f"JSprint [{sprint}] >>> "

    @property
    def current_sprint(self):
        if self.__current_sprint is None:
            self.__current_sprint = self.get_active_sprint()

        return self.__current_sprint

    @current_sprint.setter
    def current_sprint(self, value):
        self.__current_sprint = value

    def get_sprints(self, state: str = "active") -> List[Sprint]:
        return self.jira.sprints(settings.get("jira_board_id"), state=state)

    def get_active_sprint(self) -> Sprint:
        sprints = self.get_sprints(state="active")

        if len(sprints):
            # Use fisrt active sprint as default
            sprint = sorted(sprints, key=attrgetter("name"))[0]
        else:
            sprint = None

        return sprint

    # ------------------
    # Set current sprint
    # ------------------
    def help_use(self):
        print_help("""
            use [sprint_id]

            Set the current sprint.
            If no `sprint_id` is passed the current active sprint will be used.
            """)

    @do_exception
    def do_use(self, line):
        # Parse arguments
        args = shlex.split(line)

        # Select sprint
        if len(args) == 0:
            sprint = self.get_active_sprint()

            if sprint is None:
                print("No active sprint")
                return
        else:
            sprint_id = get_sprint_id_from_number(args[0])
            sprint = self.jira.sprint(sprint_id)

        print(
            f"Using sprint {Style.BRIGHT + sprint.name + Style.RESET_ALL} ({sprint.id})"
        )

        self.current_sprint = sprint

    # ------------
    # Show sprints
    # ------------
    def help_sprints(self):
        print_help("List all the active and future sprints")

    def do_sps(self, line):
        return self.do_sprints(line)

    @do_exception
    def do_sprints(self, line):
        actives = self.get_sprints(state="active")
        futures = self.get_sprints(state="future")

        sprints = futures + actives
        sprints = sorted(sprints, key=attrgetter("name"))

        for sprint in sprints:
            name = sprint.name
            id_ = Style.BRIGHT + str(sprint.id) + Style.RESET_ALL
            state = ("*" if sprint.state == "active" else
                     " ") + colored_sprint_state(sprint)

            print(f"{state} {name} ({id_})")

    # -----------
    # Show sprint
    # -----------
    def help_sprint(self):
        print_help("""
            sprint [sprint_id]

            Show the issues in the sprint grouped by assignee.
            If no `sprint_id` is given use the current selected sprint.
            """)

    def do_sp(self, line):
        return self.do_sprint(line)

    @do_exception
    def do_sprint(self, line):
        def group_by_assignee(acc, issue: Issue) -> Dict[str, Iterable[Issue]]:
            assignee = get_assignee_from_issue(issue)

            acc.setdefault(assignee, []).append(issue)

            return acc

        # Parse arguments
        args = shlex.split(line)

        # Show sprint
        if len(args) == 0:
            sprint = self.current_sprint
        else:
            sprint_id = get_sprint_id_from_number(args[0])
            sprint = self.jira.sprint(sprint_id)

        print(f"Displaying sprint {sprint.name}")

        # Show issue by assignee
        jira_project = settings.get("jira_project")
        team_members = settings.get("team_members")
        team_labels = settings.get("team_labels")

        jql = f"project = '{jira_project}' AND sprint = {sprint.id}"

        if team_members:
            jql += f" AND (assignee IS NULL or assignee IN {tuple(team_members)})"

        if team_labels:
            jql += f" AND (labels IS NULL or labels IN {tuple(team_labels)})"

        issues = self.jira.search_issues(jql)

        if len(issues):
            permalink_padding = max(len(i.permalink()) for i in issues)
            status_padding = max(len(i.fields.status.name) for i in issues)
            issue_key_padding = max(len(i.key) for i in issues)
        else:
            permalink_padding = status_padding = issue_key_padding = 0

        issues_by_user = functools.reduce(group_by_assignee, issues, {})
        assignees = sorted(issues_by_user.keys())

        for i, assignee in enumerate(assignees):
            user_issues = issues_by_user[assignee]
            user_issues = sorted(user_issues, key=attrgetter("id"))

            print(Style.BRIGHT + f"{assignee}:" + Style.RESET_ALL)

            for issue in user_issues:
                issue_key = Style.BRIGHT + issue.key.ljust(
                    issue_key_padding) + Style.RESET_ALL
                url = issue.permalink().ljust(permalink_padding)
                status = colored_issue_status(issue, status_padding)
                summary = Style.BRIGHT + issue.fields.summary + Style.RESET_ALL

                print(f"{status} - {issue_key} ({url}) {summary}")

            if i != (len(assignees) - 1):
                print()

    # ---------------------
    # Show sprint as report
    # ---------------------
    def help_report(self):
        print_help("""
            sprint [sprint_id]

            Show the issues in the sprint grouped by user for reporting.
            If no `sprint_id` is given use the current selected sprint.
            The difference with the `sprint` command is that the grouping by user is
            done by whom actually worked on the issue instead of just the assignee.
            """)

    def do_rp(self, line):
        return self.do_report(line)

    """
    @do_exception
    def do_report(self, line):
        def group_by_developers(acc, issue: Issue) -> Dict[str, Iterable[Issue]]:
            developers = get_developers_from_issue(issue)

            for developer in developers:
                acc.setdefault(developer.displayName, []).append(issue)

            return acc

        def is_developer_in_team(team_members: List[str], issue: Issue) -> bool:
            developers = get_developers_from_issue(issue)

            for developer in developers:
                if developer.name in team_members:
                    return True

            return False

        # Parse arguments
        args = shlex.split(line)

        # Show sprint
        if len(args) == 0:
            sprint = self.current_sprint
        else:
            sprint_id = get_sprint_id_from_number(args[0])
            sprint = self.jira.sprint(sprint_id)

        print(f"Displaying sprint {sprint.name}")

        # Show issue by developers
        jira_project = settings.get("jira_project")
        team_members = settings.get("team_members")

        jql = f"project = '{jira_project}' AND sprint = {sprint.id}"

        issues = self.jira.search_issues(jql)
        issues = (i for i in issues if is_developer_in_team(team_members, i))

        issues_by_developer = functools.reduce(group_by_developers, issues, {})
        developers = sorted(set(issues_by_developer.keys()))

        for i, developer in enumerate(developers):
            developer_issues = issues_by_developer[developer]
            developer_issues = sorted(developer_issues, key=attrgetter("id"))

            print(Style.BRIGHT + f"{developer}:" + Style.RESET_ALL)

            for issue in developer_issues:
                url = issue.permalink()
                summary = Style.BRIGHT + issue.fields.summary + Style.RESET_ALL

                print(f"- {url} {summary}")

            if i != (len(developers) - 1):
                print()
    """

    @do_exception
    def do_report(self, line):
        def group_by_assignee(acc, issue: Issue) -> Dict[str, Iterable[Issue]]:
            assignee = get_assignee_from_issue(issue)

            acc.setdefault(assignee, []).append(issue)

            return acc

        # Parse arguments
        args = shlex.split(line)

        # Show sprint
        if len(args) == 0:
            sprint = self.current_sprint
        else:
            sprint_id = get_sprint_id_from_number(args[0])
            sprint = self.jira.sprint(sprint_id)

        print(f"Displaying sprint {sprint.name}")

        # Show issue by assignee
        jira_project = settings.get("jira_project")
        team_members = settings.get("team_members")
        team_labels = settings.get("team_labels")

        jql = f"project = '{jira_project}' AND sprint = {sprint.id} AND resolution = 'Unresolved'"

        if team_members:
            jql += f" AND (assignee IS NULL or assignee IN {tuple(team_members)})"

        if team_labels:
            jql += f" AND (labels IS NULL or labels IN {tuple(team_labels)})"

        issues = self.jira.search_issues(jql)
        issues_by_user = functools.reduce(group_by_assignee, issues, {})
        assignees = sorted(issues_by_user.keys())

        status_padding = max(len(i.fields.status.name)
                             for i in issues) if len(issues) else 0

        for i, assignee in enumerate(assignees):
            user_issues = issues_by_user[assignee]
            user_issues = sorted(user_issues,
                                 key=attrgetter("fields.customfield_14560"))

            print(Style.BRIGHT + f"{assignee}:" + Style.RESET_ALL)

            for issue in user_issues:
                url = issue.permalink()
                status = colored_issue_status(issue, status_padding)
                summary = Style.BRIGHT + issue.fields.summary + Style.RESET_ALL

                print(f"- {url} ({status}) {summary}")

            if i != (len(assignees) - 1):
                print()

    # --------------------
    # Assign user to issue
    # --------------------
    def help_assign(self):
        print_help("""
            assign <issue_number> <assignee>

            Assign the issue to an user.
            """)

    def complete_a(self, *args):
        return self.complete_assign(*args)

    def do_a(self, *args):
        return self.do_assign(*args)

    @do_exception
    def complete_assign(self, text, line, begin_index, end_index):
        s = text.lower()
        matches = filter(lambda x: x.startswith(s),
                         settings.get("team_members"))

        return list(matches)

    @do_exception
    def do_assign(self, line):
        # Parse args
        args = shlex.split(line)

        if len(args) < 2:
            print("Need two arguments: issue number and assignee name")
            return

        # Assign issue
        issue_key = get_issue_key_from_number(args[0])
        assignee = args[1]

        self.jira.assign_issue(issue_key, assignee)

    # ----------------------
    # Unassign user to issue
    # ----------------------
    def help_unassign(self):
        print_help("""
            unassign <issue_number>

            Remove assignee from the given issue.
            """)

    def do_u(self, line):
        return self.do_unassign(line)

    @do_exception
    def do_unassign(self, line):
        # Parse args
        args = shlex.split(line)

        if len(args) == 0:
            print("Needs at least an issue number")
            return

        # Unassign issue
        issue_key = get_issue_key_from_number(args[0])

        self.jira.assign_issue(issue_key, None)

    # ---------------------------
    # Add issue to current sprint
    # ---------------------------
    def help_add(self):
        print_help("""
            add <issue_number>

            Add issue to the current sprint.
            """)

    @do_exception
    def do_add(self, line):
        # Parse args
        args = shlex.split(line)

        if len(args) == 0:
            print("Needs at least an issue number")
            return

        # Add issue to sprint
        issue_keys = [get_issue_key_from_number(arg) for arg in args]
        sprint = self.current_sprint

        self.jira.add_issues_to_sprint(sprint.id, issue_keys)

    # -----------------------------
    # Move or add issue to a sprint
    # -----------------------------
    def help_move(self):
        print_help("""
            move <sprint_id> <issue_number>

            Move an issue into a sprint.
            """)

    def do_mv(self, line):
        return self.do_move(line)

    @do_exception
    def do_move(self, line):
        # Parse args
        args = shlex.split(line)

        if len(args) < 2:
            print("Needs a sprint ID and at least one issue number")
            return

        # Move issues
        sprint_id = get_sprint_id_from_number(args[0])
        issue_keys = [get_issue_key_from_number(arg) for arg in args]

        self.jira.add_issues_to_sprint(sprint_id, issue_keys)

    # ---------------
    # Show issue info
    # ---------------
    def help_show(self):
        print_help("""
            show <issue_number>

            Show informations about the given issue number
            """)

    def do_sh(self, line):
        return self.do_show(line)

    @do_exception
    def do_show(self, line):
        # Parse args
        args = shlex.split(line)

        if len(args) < 1:
            print("Needs at least one issue number")
            return

        # Show issue stats
        issue_key = get_issue_key_from_number(args[0])
        issue = self.jira.issue(issue_key,
                                "assignee,status,summary,customfield_11360")
        status = colored_issue_status(issue)
        up_in_build = get_up_in_build_form_issue(issue)

        print(
            f"Issue       : {Style.BRIGHT + issue.key + Style.RESET_ALL} ({issue.permalink()})"
        )
        print(f"Status      : {Style.BRIGHT + status + Style.RESET_ALL}")
        print(
            f"Assignee    : {Style.BRIGHT + get_assignee_from_issue(issue) + Style.RESET_ALL}"
        )
        print(f"Up in build : {Style.BRIGHT + up_in_build + Style.RESET_ALL}")
        print(
            f"Summary     : {Style.BRIGHT + issue.fields.summary + Style.RESET_ALL}"
        )

    # -----------------------
    # Move issue into backlog
    # -----------------------
    def help_backlog(self):
        print_help("""
            backlog <issue_number>

            Move an issue out of the sprint and back into backlog.
            """)

    def do_bk(self, line):
        return self.do_backlog(line)

    @do_exception
    def do_backlog(self, line):
        # Parse args
        args = shlex.split(line)

        if len(args) < 1:
            print("Needs at least one issue number")
            return

        # Get active sprint
        issue_keys = [get_issue_key_from_number(arg) for arg in args]

        # Move issue into backlog
        self.jira.move_to_backlog(issue_keys)

    # ----------------
    # Leave the REPL
    # ----------------
    def do_quit(self, line):
        return True

    def do_q(self, args):
        return True

    def do_EOF(self, line):
        return True