Beispiel #1
0
    def get_issues(self, project: str):
        options = {'server': self.server}

        jira = JIRA(options, basic_auth=(self.user, self.api_key))

        jira_dashboards = jira.boards()

        board = [
            jira_dashboard for jira_dashboard in jira_dashboards
            if jira_dashboard.name == project
        ]

        jra = jira.sprints(board[0].id)

        active = [sprint for sprint in jra if sprint.state == 'ACTIVE']

        sprint_info = jira.sprint(active[0].id)

        all_sprint_issues = jira.search_issues(
            f'project={project} and sprint = {sprint_info.id}')

        issues = []

        for issue in all_sprint_issues:
            if str(issue.fields.issuetype) != 'Technical Task':
                issues.append({
                    'key': issue.key,
                    'type': str(issue.fields.issuetype),
                    'status': str(issue.fields.status),
                    'summary': issue.fields.summary
                })

        return issues
Beispiel #2
0
class JiraUtils:
    def __init__(self, url, username, password, gateway):
        self.jira = JIRA(url, basic_auth=(username, password))
        self.gateway = gateway

    def get_available_statuses(self):
        issues = self.jira.search_issues('assignee="Ryan Long"')
        return list(
            set([(issue.fields.status.id, issue.fields.status.name)
                 for issue in issues]))

    def get_issues_with_status(self, status):
        issues = self.jira.search_issues(
            'project= "Affiliate Network" AND status={}'.format(status))
        return issues

    def get_projects_assigned_to(self, assignee):
        issues = self.jira.search_issues(
            'project= "Affiliate Network" and assignee="{}"'.format(assignee))
        return issues

    def boards(self):
        return self.jira.boards()

    def run_command(self, command, args):
        if args is None:
            args = []
        command = self.gateway.getCommand(command)
        print(command.instruction.format(*args))
        result_set = [
            x for x in (getattr(self.jira, command.type_name)(
                command.instruction.format(*args)))
        ]

        results = {}
        results['id'] = [x for x in result_set]
        for field in command.fields:
            results[field] = [getattr(x.fields, field) for x in result_set]
            if field in ["updated", "created"]:
                results[field] = [
                    datetime.strptime(x[:19], '%Y-%m-%dT%H:%M:%S')
                    for x in results[field]
                ]
        return results

    def get_detailed_commands_list(self):
        return {
            'Command': [x[0] for x in self.gateway.get_list_of_commands()],
            'Query': [x[1] for x in self.gateway.get_list_of_commands()],
            'Return Fields':
            [x[2] for x in self.gateway.get_list_of_commands()]
        }

    def get_commands_list(self):
        return [x[0] for x in self.gateway.get_list_of_commands()]
Beispiel #3
0
class JiraService:
    def __init__(self, jira_config):
        self.client = JIRA(jira_config.address,
                           basic_auth=(jira_config.basic_auth_user,
                                       jira_config.basic_auth_pass))

    def get_projects(self):
        projects = self.client.projects()
        keys = sorted([project.key for project in projects])[2:5]
        return projects

    def get_boards(self, name):
        boards = self.client.boards()
        return list(filter(lambda x: name in x.name, boards))
Beispiel #4
0
def update_sprint():
    try:
        jira_instance = JIRA(server=url_bench.jira_url, basic_auth=(url_bench.host, url_bench.password))
    except Exception:
        return
    boards = jira_instance.boards()
    sprint_info_dict = {}
    sprints = []
    projects = Projects.objects.filter()
    for project in projects:
        for board in boards:
            if project.project_name.capitalize() in board.name.capitalize():
                sprint_info_dict['board'] = board.name
                sprints = jira_instance.sprints(board.id)

        for sprint in sprints:
            if sprint.state == 'ACTIVE' and not Sprints.objects.filter(sprint_name=sprint.name, is_active=1).exists():
                Sprints.objects.create(sprint_name=sprint.name, project=project, start_date=datetime.now(),
                                       is_active=1)
Beispiel #5
0
def update_active_sprint_summary():
    if Sprints.objects.filter(is_active=True).exists():
        SprintSummary.objects.filter().delete()
        try:
            jira_instance = JIRA(server=url_bench.jira_url, basic_auth=(url_bench.host, url_bench.password))
        except Exception:
            return
        boards = jira_instance.boards()
        sprint_info_dict = {}
        sprints = []
        for board in boards:
            if 'mPulse' in board.name:
                sprint_info_dict['board'] = board.name
                sprints = jira_instance.sprints(board.id, state='ACTIVE')

        active_sprints_obj = filter(lambda sprint: sprint.state == 'ACTIVE', sprints)

        if active_sprints_obj:
            for active_sprint in active_sprints_obj:
                sprint_info_dict['active_sprint'] = active_sprint.name
                sprint_info_dict['active_sprint_id'] = active_sprint.id
        issues_list = jira_instance.search_issues('sprint=%s'%sprint_info_dict['active_sprint_id'])
        update_summary = []
        for issue in issues_list:
            sprint_instance = {}
            sprint_instance['ticket'] = issue.key or ''
            issue_ob = jira_instance.issue(issue.key)
            sprint_instance['ticket_desc'] = issue_ob.fields.summary or ''
            sprint_instance['assignee'] = issue_ob.fields.assignee or ''
            sprint_instance['due_date'] = issue_ob.fields.duedate or ''
            sprint_instance['status'] = issue_ob.fields.status.name or ''
            sprint_instance['issue_type'] = issue_ob.fields.issuetype.name or ''
            sprint_instance['reporter'] = issue_ob.fields.reporter.name
            if hasattr(issue_ob.fields, 'customfield_10013'):
                sprint_instance['points'] = int(issue_ob.fields.customfield_10013 or 0)
            sprint_instance['sprint_name'] = sprint_info_dict['active_sprint']
            update_summary.append(SprintSummary(**sprint_instance))
        SprintSummary.objects.bulk_create(update_summary)
from jira import JIRA
import requests
from config import JIRA_DOMAIN, JIRA_PASSWORD, JIRA_USERNAME


jira = JIRA(JIRA_DOMAIN, basic_auth=(JIRA_USERNAME, JIRA_PASSWORD))

projects = jira.projects()

boards = jira.boards()
for board in boards:
	print('BOARD: ', board)

dashboards = jira.dashboards()
for dash in dashboards:
	print(dash.name, dash.id)


cizo_dashboard = jira.dashboard(id=11105)
# curago_dashboard = jira.dashboard('CURAGO')

cizo_dash_page = requests.get(cizo_dashboard.view).text

print(cizo_dash_page)





for project in projects:
	print (project)
Beispiel #7
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)
Beispiel #8
0
def ensure_board(jira: JIRA):
    return bool(jira.boards())
def main():
	jira = JIRA(options, basic_auth=credentials)
	#print(dir(jira))
	board = jira.boards()[0]
	sprint = jira.sprints(board.id)[0]
	print(board)
	print(dir(board))
	print("########################################################################")
	print(sprint)
	print(dir(sprint))
	#sprints = jira.sprints('10710')
	'''projects = jira.projects()
	data_dict = {}
	for project in projects[:2]:
		print ( project.id + " : " + project.name )
		data_dict[project.name] = {}
		data_dict[project.name]['id'] = project.id
		sprints = requests.get( options['server'] + '/rest/greenhopper/1.0/integration/teamcalendars/sprint/list?jql=project=' + project.id, auth=credentials ).json()['sprints']
		durationList = []
		for sprint in sprints:
			sprint1 = jira.sprint(sprint['id'])
			data_dict[project.name][sprint1.name] = {}
			data_dict[project.name][sprint1.name]['id'] = sprint1.id
			strtD = sprint1.startDate
			endD = sprint1.endDate
			comD = sprint1.completeDate
			print("  " + str(sprint['id']) + " : " + sprint['name'])
			print("    -------------------------------------------------------------")
			print("    Start Date : " + strtD)
			data_dict[project.name][sprint1.name]['start_time'] = strtD
			print("    Estimated End Date : " + endD)
			data_dict[project.name][sprint1.name]['end_time'] = endD
			print("    Completed Date :" + comD)
			data_dict[project.name][sprint1.name]['complete_time'] = comD
			if strtD != 'None' and comD != 'None':	
				startl = strtD[:9].split('/')
				compl = comD[:9].split('/')
				date1 = date(int('20' + startl[2]),cal[startl[1]],int(startl[0]))
				date2 = date(int('20' + compl[2]),cal[compl[1]],int(compl[0]))
				duration = (date2-date1).days
				durationList.append(duration)
				data_dict[project.name][sprint1.name]['sprint_duration'] = str(duration) + ' Days'
				print("    Duration : " + str(duration) + " Days")
			else:
				print("    Ongoing Sprint")
			print("    -------------------------------------------------------------\n")
		try:
			avgDuration = sum(durationList) / len(durationList)
			data_dict[project.name]['average_sprint_duration'] = avgDuration
			print("  Average Sprint Duration : " + str(avgDuration))
		except ZeroDivisionError:
			print("  Average Sprint Duration : 14 days ")
			data_dict[project.name]['average_sprint_duration'] = 14.0
	json_data = json.dumps(data_dict, indent=4)
	with open('result.json', 'w') as fp:
		json.dump(data_dict, fp, indent=4)'''
   	#print(str(sprint) + "\n\n")
	# gh = GreenHopper(options, basic_auth=('*****@*****.**', 'cA1AvOAgGdMaccejfgwy8919'))
	# boards = gh.boards()

	# print (str(dir(boards[0])) + "\n\n")

	# sprints = gh.sprints(boards[0].id)

	# for sprint in sprints:
	# 	print(sprint.name)



	'''print(jira)
	issue = jira.issue()
	print (issue.fields.assignee)
	print (issue.fields.reporter)'''

	#proj = jira.project('SAM')
	'''j=1
	issues=jira.search_issues('project=DC',maxResults=3)#gives the first 0 issues
	print("Issues...")
	for issue in issues:
		print (issue.fields.created)
		print (issue.fields.status.name)
		j+=1
	Projects,i = [],1
	for project in jira.projects():
		print (str(i) + " Key :" + project.key + " Name : " + project.name)
		i+=1'''
	#print(Projects)
	#print(dir(jira.issue('DC-214')))

	#print("\n\n")
	#print(dir(jira.project('DC')))
	#print(jira.sprints( jira.boards()[0].id ))		

	#jiraResponse1 = requests.get('https://altimetrik.atlassian.net/rest/api/2/project/DC', auth=('*****@*****.**','cA1AvOAgGdMaccejfgwy8919')).json()
	#jiraResponse2 = requests.get('https://altimetrik.atlassian.net/rest/agile/1.0/sprint/468', auth=('*****@*****.**','cA1AvOAgGdMaccejfgwy8919')).json()

	#jiraResponse3 = requests.get('https://altimetrik.atlassian.net/rest/greenhopper/1.0/integration/teamcalendars/issue/list?jql=project=DC', auth=('*****@*****.**','cA1AvOAgGdMaccejfgwy8919')).json()
	#print(jiraResponse3)
	#print(jiraResponse3['sprints'][0])

	'''
	'_get_url', '_load', '_options', '_parse_raw', '_resource', '_session', 'canUpdateSprint', 'completeDate', 'daysRemaining',
	 'delete', 'endDate', 'find', 'goal', 'id', 'isoCompleteDate', 'isoEndDate', 'isoStartDate', 'linkedPagesCount', 'name', 
	 'raw', 'remoteLinks', 'self', 'sequence', 'startDate', 'state', 'update']

	'''

	#print(jiraResponse2['values'][468]['startDate'][:10])
	#print(jira.sprint(468).startDate)
	#print(jira.sprint(468).endDate)
	'''sprint1 = jira.sprint(606)
Beispiel #10
0
class jira_handler:
    """
    Jira处理类。
    【备注】:目前存在Jira方法与Issue对象混合在一起的不足。
    【改进】:将Jira方法与Issue对象实体分离,各自进行类定义。
    """

    def __init__(self, project_name):
        self.mongo_db = mongodb_class.mongoDB(project_name)
        self.jira = JIRA('http://172.16.60.13:8080', basic_auth=('shenwei','sw64419'))
        self.gh = GreenHopper({'server': 'http://172.16.60.13:8080'}, basic_auth=('shenwei', 'sw64419'))
        self.name = project_name
        self.project = self.jira.project(self.name)
        self.pj_name = u"%s" % self.project.name
        self.pj_manager = u"%s" % self.project.lead.displayName
        """获取项目版本信息
        """
        _versions = self.jira.project_versions(self.name)
        self.version = {}
        self.sprints = self._get_sprints()
        for _v in _versions:
            _key = (u"%s" % _v).replace('.', '^')
            if not self.version.has_key(_key):
                self.version[_key] = {}
            self.version[_key][u"id"] = _v.id
            self.version[_key]['startDate'] = ""
            self.version[_key]['releaseDate'] = ""
            if 'startDate' in dir(_v):
                self.version[_key]['startDate'] = _v.startDate
            if 'releaseDate' in dir(_v):
                self.version[_key]['releaseDate'] = _v.releaseDate
            self.mongo_db.handler("project", "update",
                                  {"version": _key}, dict({"version": _key}, **self.version[_key]))
        self.issue = None

    def _get_board(self):
        _boards = self.jira.boards()
        for _b in _boards:
            if self.name.upper() in _b.name.upper():
                return _b.id
        return None

    def transDate(self, str):
        print("---> transDate [%s] <---" % str)
        if str != None and str != u'无':
            _s = str.\
                replace(u'十一月', '11').\
                replace(u'十二月', '12').\
                replace(u'一月', '1').\
                replace(u'二月', '2').\
                replace(u'三月', '3').\
                replace(u'四月', '4').\
                replace(u'五月', '5').\
                replace(u'六月', '6').\
                replace(u'七月', '7').\
                replace(u'八月', '8').\
                replace(u'九月', '9').\
                replace(u'十月', '10')
            _time = time.strptime(_s, '%d/%m/%y')
            return time.strftime('%Y-%m-%d', _time)
        else:
            return ""

    def _get_sprints(self):
        """
        获取看板内sprint列表
        :return: sprint列表 [ name, startDate, endDate, state ]
        """
        _list = []
        _b_id = self._get_board()
        if type(_b_id) is not types.NoneType:
            _sprints = self.jira.sprints(_b_id)
            for _s in _sprints:
                _sprint = self.jira.sprint(_s.id)
                _data = {'name': _s.name,
                         'startDate': self.transDate(_sprint.startDate.split(' ')[0]),
                         'endDate': self.transDate(_sprint.endDate.split(' ')[0]),
                         'state': _s.state
                         }
                _list.append(_data)
            return _list
        return None

    def get_sprints(self):
        return self.sprints

    def get_current_sprint(self):
        """
        获取本阶段sprint名称
        :return: 返回状态为ACTIVE的sprint的名称
        """
        if type(self.sprints) is not types.NoneType:
            for _s in self.sprints:
                if _s['state'] == 'ACTIVE':
                    return (_s['name'], _s['startDate'], _s['endDate']), _next
                _next = (_s['name'], _s['startDate'], _s['endDate'])
        return None

    def get_sprint(self):
        """
        获取当前Issue的sprint定义
        :return: sprint定义
        """
        if "customfield_10501" in self.issue.raw['fields'] and \
                type(self.issue.fields.customfield_10501) is not types.NoneType:
            return u'%s' % (",".join(item.split('name=')[1].split(',')[0]
                                      for item in self.issue.fields.customfield_10501))
            # return u'%s' % self.issue.fields.customfield_10501[0].split('name=')[1].split(',')[0]
        return None

    def get_versions(self):
        _v = {}
        for _k in self.version:
            _key = (u"%s" % _k).replace('^', '.')
            _v[_key] = self.version[_k]
        return _v

    def get_pj_info(self):
        return {'pj_name': self.pj_name, 'pj_manager': self.pj_manager}

    def set_issue_by_name(self, issue_id):
        self.issue = self.jira.issue(issue_id)

    def print_green_hopper(self):
        _f = self.gh.fields()
        for __f in _f:
            __cns = __f['clauseNames']
            print('-' * 8)
            for _n in __cns:
                print u"name: %s" % _n
            print "id: ", u"%s" % __f['id']
            print "name: ", u"%s" % __f['name']

    def get_story_point(self):
        """
        获取Issue(story)的预置成本, 1 point = 4 hours
        :return: 预置成本
        """
        if "customfield_10304" in self.issue.raw['fields'] and \
                type(self.issue.fields.customfield_10304) is not types.NoneType:
            return self.issue.fields.customfield_10304
        return None

    def get_task_time(self):
        return {"agg_time": self.issue.fields.aggregatetimeestimate,
                "org_time": self.issue.fields.timeoriginalestimate,
                "spent_time": self.issue.fields.timespent}

    def get_landmark(self):
        if len(self.issue.fields.fixVersions) > 0:
            return u"%s" % self.issue.fields.fixVersions[0]
        if len(self.issue.fields.versions) > 0:
            # print self.show_name(), " version: %s" % self.issue.fields.versions[0]
            return u"%s" % self.issue.fields.versions[0]
        return ""

    def get_desc(self):
        return self.issue.fields.summary

    def show_name(self):
        return str(self.issue)

    def get_type(self):
        return u"%s" % self.issue.fields.issuetype

    def get_status(self):
        """
        获取Issue的状态,待办、处理中、待测试、测试中、完成
        :return:
        """
        return u"%s" % self.issue.fields.status

    def get_subtasks(self):
        """
        收集issue的相关子任务的issue
        :return: 相关issue字典
        """
        link = {}
        if not link.has_key(self.show_name()):
            link[self.show_name()] = []
        _task_issues = self.issue.fields.subtasks
        for _t in _task_issues:
            link[self.show_name()].append(u"%s" % _t)
        return link

    def get_child_requirement(self):

        link = []
        jql = "issue in  childrenOfParentRequirement('%s')" % self.show_name()
        # print jql
        tot = 0
        while True:
            issues = self.jira.search_issues(jql, maxResults=100, startAt=tot)
            for issue in issues:
                link.append(issue.key)
            if len(issues) == 100:
                tot += 100
            else:
                break
        return link

    def get_epic_link(self, jql):

        print(">>> get_epic_link<%s>" % jql)
        total = 0
        _issue_name = self.show_name()
        task_link = {_issue_name: []}
        while True:
            issues = self.jira.search_issues(jql, maxResults=100, startAt=total)
            for issue in issues:
                self.issue = issue
                self.sync_issue()
                """收集epic相关的story和任务"""
                task_link[_issue_name].append(self.show_name())
            if len(issues) == 100:
                total += 100
            else:
                break
        print task_link
        self.set_issue_by_name(_issue_name)
        return task_link

    def get_link(self):
        """
        收集issue的相关issue
        :return: 相关issue字典
        """
        link = {self.show_name(): []}

        """兼容以前: 与story相关的task是通过issulelinks关联的"""
        _task_issues = self.issue.fields.issuelinks
        for _t in _task_issues:
            if "outwardIssue" in dir(_t):
                """该story相关的任务"""
                link[self.show_name()].append(u"%s" % _t.outwardIssue)
            if "inwardIssue" in dir(_t):
                """该story相关的任务"""
                link[self.show_name()].append(u"%s" % _t.inwardIssue)

        """采用synapseRT插件后对需求的管理"""
        _task_issues = self.get_child_requirement()
        for _t in _task_issues:
            link[self.show_name()].append(_t)

        return link

    def sync_issue(self):
        """
        同步issue数据,同时完成重要参量的变更日志。
        :return:
        """
        _components = u"%s" % (', '.join(comp.name for comp in self.issue.fields.components))
        _key = u"%s" % self.show_name()
        _time = self.get_task_time()
        _epic_link = None
        if "customfield_11300" in self.issue.raw['fields'] and \
                type(self.issue.fields.customfield_11300) is not types.NoneType:
            _epic_link = self.issue.raw['fields']["customfield_11300"]
        _issue = {u"%s" % self.show_name(): {
                "issue_type": self.get_type(),
                "created": self.issue.fields.created,
                "updated": self.issue.fields.updated,
                "lastViewed": self.issue.fields.lastViewed,
                "users": self.get_users(),
                "status": self.get_status(),
                "landmark": self.get_landmark(),
                "point": self.get_story_point(),
                "agg_time": _time['agg_time'],
                "org_time": _time['org_time'],
                "summary": self.issue.fields.summary,
                "spent_time": _time['spent_time'],
                "sprint": self.get_sprint(),
                "epic_link": _epic_link,
                "components": _components
            }}
        _old_issue = self.mongo_db.handler("issue", "find_one", {"issue": _key})
        if _old_issue is None:
            self.mongo_db.handler("issue", "update",
                                  {"issue": _key}, dict({"issue": _key}, **_issue[_key]))
        else:
            _change = False
            for _item in ['issue_type','created','updated','users','status',
                          'landmark','point','agg_time','org_time',
                          'summary','spent_time','sprint','epic_link']:
                if _old_issue[_item] != _issue[_key][_item]:
                    _log = {"issue_id": _key, "key": _item,
                            "old": _old_issue[_item], "new": _issue[_key][_item]}
                    self.write_log(_log)
                    _change = True
            _change = True
            if _change:
                self.mongo_db.handler("issue", "update",
                                      {"issue": _key}, dict({"issue": _key}, **_issue[_key]))

    def get_issue_link(self):
        _link = self.get_link()
        print "---> get_issue_link: ", _link
        return _link

    def sync_issue_link(self):
        _key = u"%s" % self.show_name()
        _link = self.get_link()

        print "sync_issue_link: ", _link

        self.mongo_db.handler("issue_link", "update",
                              {"issue": _key}, dict({"issue": _key}, **_link))
        return _link

    def show_issue(self):
        """
        显示issue信息
        :return:
        """
        print(u"[%s]" % self.show_name()),
        print u"类型:%s" % self.get_type(),
        print(u'状态:%s' % self.get_status()),
        print u"里程碑:%s" % self.get_landmark()

    def get_users(self):
        """
        获取访问issue的用户
        2018.3.1:改为 经办人 assignee
        :return:
        watcher = self.jira.watchers(self.issue)
        _user = u"%s" % (', '.join(watcher.displayName for watcher in watcher.watchers))
        """
        if type(self.issue.raw['fields']["assignee"]) is types.NoneType:
            return None
        return (u"%s" % self.issue.raw['fields']["assignee"]['displayName']).replace(' ', '')

    def write_log(self, info):
        print "---<write_log>---: ",info
        self.mongo_db.handler("log", "insert", info)

    def write_worklog(self, info):
        """
        写入或更新 工作日志记录。
        :param info: 新的日志数据
        :return:
        """
        if not info.has_key("comment"):
            return

        _search = {'issue': info['issue'],
                   'id': info['id']}
        self.mongo_db.handler('worklog', 'update', _search, info)

    def clear_worklog(self, worklog_id):
        """
        清除"不存在"的记录!
        :param worklog_id: 存在的worklog_id
        :return:
        """
        _set = {'timeSpentSeconds': 0}
        if len(worklog_id) > 0:
            _search = {"issue": self.show_name(), "id": {"$not": {"$in": worklog_id}}}
        else:
            _search = {"issue": self.show_name()}
        _one = self.mongo_db.handler('worklog', 'find_one', _search)
        if _one is None:
            return
        # self.mongo_db.handler('worklog', 'remove', _search)
        """保留原记录,将其用时值设置为0,以便事后跟踪
        """
        self.mongo_db.handler('worklog', 'update', _search, _set)

    def sync_worklog(self):
        """
        获取指定 issue 的工作日志记录。
        - 2018.4.2:针对以前有的,但现在没有的 日志记录 的处理?!清除其spent时间
        :return:
        """
        worklogs = self.jira.worklogs(self.show_name())
        wl = {}
        _id = []
        for worklog in worklogs:
            wl['issue'] = self.show_name()
            wl['author'] = u'%s' % worklog.author
            wl['comment'] = u'%s' % worklog.comment
            wl['timeSpent'] = worklog.timeSpent
            wl['timeSpentSeconds'] = worklog.timeSpentSeconds
            wl['updated'] = worklog.updated
            wl['created'] = worklog.created
            wl['started'] = worklog.started
            wl['id'] = worklog.id
            _id.append(worklog.id)
            self.write_worklog(wl)
        """同时同步Issue的变动日志"""
        """ 因worklog可随意更改或删除, 有必要实时清除多余的记录!"""
        self.clear_worklog(_id)

        self.sync_changelog()

    def scan_task_by_sprint(self, sprint):
        """
        通过sprint获取Issue,以便获取它们的 工作日志
        :param sprint: 当前的sprint名称
        :return: Issue列表
        """
        jql_sql = u'project=%s AND Sprint = "%s" ORDER BY created DESC' %\
                  (self.name, sprint)
        total = 0
        tasks = []
        while True:
            issues = self.jira.search_issues(jql_sql, maxResults=100, startAt=total)
            for issue in issues:
                self.issue = issue
                """同步issue"""
                self.sync_issue()
                self.sync_worklog()
                self.sync_issue_link()
                tasks.append(self.show_name())
            if len(issues) == 100:
                total += 100
            else:
                break
        return tasks

    def scan_epic(self, bg_date):
        """
        扫描project收集epic信息
        :param bg_date: 起始日期,如 2018-1-31
        :param issue_type:指定issue类型
        :return: 按issue类型进行统计值kv_sum,issue链kv_link,相关任务链task_link
        """
        jql_sql = u'project=%s AND issuetype=epic AND created >= %s ORDER BY created DESC' % (
            self.name, bg_date)
        total = 0
        story_link = []

        while True:
            issues = self.jira.search_issues(jql_sql, maxResults=100, startAt=total)
            for issue in issues:
                self.issue = issue
                """同步issue"""
                # self.show_issue()
                self.sync_issue()
                """收集epic相关的story和任务"""
                _jql = u'project=%s AND "Epic Link"=%s AND created >= %s ORDER BY created DESC' % \
                       (self.name, self.show_name(), bg_date)
                _link = self.get_epic_link(_jql)
                """同步epic的link"""
                # print "--> epic link: ", dict({"issue": self.show_name()}, **_link)
                self.mongo_db.handler("issue_link", "update",
                                      {"issue": self.show_name()},
                                      dict({"issue": self.show_name()}, **_link))
                story_link += _link[self.show_name()]

            if len(issues) == 100:
                total += 100
            else:
                break
        return story_link

    def scan_story(self, bg_date):
        """
        按 project 获取其下所有 story 数据。
        :param bg_date: 开始搜索的日期
        :return:
        """
        jql_sql = u'project=%s AND issuetype=story AND created >= %s ORDER BY created DESC' % (
            self.name, bg_date)
        total = 0
        task_link = []

        while True:
            issues = self.jira.search_issues(jql_sql, maxResults=100, startAt=total)
            for issue in issues:
                self.issue = issue
                """同步issue"""
                # self.show_issue()
                self.sync_issue()
                """收集story相关的任务"""
                _link = self.sync_issue_link()
                """同步epic的link"""
                self.mongo_db.handler("issue_link", "update",
                                      {"issue": self.show_name()},
                                      dict({"issue": self.show_name()}, **_link))
                task_link += _link[self.show_name()]

            if len(issues) == 100:
                total += 100
            else:
                break
        return task_link

    def scan_task(self, bg_date):
        """
        按 project 获取其下所有与执行相关的 issue 数据。
        :param bg_date: 开始搜索的日期
        :return:
        """
        jql_sql = u'project=%s AND ( issuetype=task OR' \
                  u' issuetype=任务 OR' \
                  u' issuetype=故障 OR' \
                  u' issuetype=Bug OR' \
                  u' issuetype=Sub-task OR' \
                  u' issuetype=子任务 ) AND' \
                  u' created >= %s ORDER BY created DESC' % (self.name, bg_date)
        print jql_sql
        total = 0
        task_link = []

        while True:
            issues = self.jira.search_issues(jql_sql, maxResults=100, startAt=total)
            for issue in issues:
                self.issue = issue
                """同步issue"""
                self.show_issue()
                self.sync_issue()
                # self.sync_changelog()
                self.sync_worklog()
                task_link.append(self.show_name())
            if len(issues) == 100:
                total += 100
            else:
                break
        return task_link

    def sync_changelog(self):
        """
        获取指定 issue 的 变更日志记录
        :return:
        """
        issue = self.jira.issue(self.show_name(), expand='changelog')
        changelog = issue.changelog
        for history in changelog.histories:
            for item in history.items:
                _data = {'issue': self.show_name(),
                         'field': item.field,
                         'author': u"%s" % history.author,
                         'date': history.created,
                         'old': getattr(item, 'fromString'),
                         'new': getattr(item, 'toString')
                         }
                if self.mongo_db.handler('changelog', 'find_one', _data) is None:
                    self.mongo_db.handler('changelog', 'insert', _data)
Beispiel #11
0
class Project:
    INCLUDE_LABELS = ["21.10"]

    def __init__(self, server, api_key, email, project, dry_run=False):
        self._jira = JIRA(server,
                          basic_auth=(email, api_key),
                          options={"agile_rest_path": "agile"})
        self._project = project
        self.board = self._jira.boards(projectKeyOrID=project)[0]
        self.logger = Logger()
        self.dry_run = dry_run

    @property
    def all_issues(self):
        if self._all_issues is None:
            self._all_issues = self.search()
        return self._all_issues

    def search(self, jql="", sort="", sanitize=True):
        """Perofrm a JQL search on the project"""

        query = [f"project={self._project}"]

        if jql:
            query.append("AND")
            query.append(jql)
        if sort:
            query.append(sort)
        else:
            query.append("ORDER BY created DESC")

        query = " ".join(query)
        self.logger.debug(f"Performing Search: {query}")
        return self._jira.search_issues(query)

    def links(self, issue):
        """Find all links for on a given issue."""
        return {
            link.raw["object"]["url"]
            for link in self._jira.remote_links(issue)
        }

    def sprint(self, state):
        """Return the most recent sprint of the given state, or None."""
        sprints = self._jira.sprints(self.board.id, state=state)
        sprints.sort(key=attrgetter("startDate"), reverse=True)
        return sprints[0] if sprints else None

    def _build_labels(self, pr):
        """Build a list of the appropriate labels based on PR state."""
        labels = [Labels.EXT_PR]
        if pr.reason == "follow-up":
            labels.append(Labels.FOLLOW_UP)
        if pr.review_state == "APPROVED":
            labels.append(Labels.REVIEW_ACK)
        if pr.review_state == "CHANGES_REQUESTED":
            labels.append(Labels.REVIEW_NAK)
        if pr.merge_state == "blocked":
            labels.append(Labels.MERGE_BLOCKED)
        if pr.merge_state == "dirty":
            labels.append(Labels.MERGE_DIRTY)
        if pr.merge_state == "unstable":
            labels.append(Labels.MERGE_UNSTABLE)
        return labels

    def import_external_prs(self, prs):
        """Create project issues given trello exports"""
        active_sprint = self.sprint("active")
        if not active_sprint:
            self.logger.error(f"No active sprint for {self._project}")
            return
        issues = {
            task.fields.summary: task
            for task in self.search(f"labels = {Labels.EXT_PR}")
        }
        for pr in prs:
            jira_title = f"{pr.title} ({pr.repo_name} #{pr.number})"
            labels = self._build_labels(pr)
            if issue := issues.get(jira_title):
                self.logger.debug(
                    f"Found existing issue {issue.key}: {jira_title}")
                if issue.fields.status.name in {Lanes.REVIEW, Lanes.DONE}:
                    self.move_to_lane(issue, Lanes.TODO)
                    self.add_comment(issue, f"Needs review: {pr.reason}")
                self.ensure_labels(issue, labels)
            else:
                issue = self.create_issue({
                    "summary": jira_title,
                    "description": pr.body,
                    "labels": labels,
                    Fields.SPRINT: active_sprint.id,
                    "issuetype": {
                        "name": IssueTypes.TASK
                    },
                })
                self.add_comment(issue, f"Needs review: {pr.reason}")
            self.ensure_link(issue, pr.url)
Beispiel #12
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
Beispiel #13
0
class JiraHelper:
    def __init__(self, host, user, password):
        self.jira = JIRA(host, basic_auth=(user, password), max_retries=1)
        self.ddp = DateDataParser(languages=['pt', 'en'])

    def get_projects(self):
        return self.jira.projects()

    def get_boards_by_project_key(self, key_project):
        allboards = self.jira.boards(maxResults=1000, type='scrum')
        boards = []
        for board in allboards:
            projects = []
            try:
                projects = board.raw['filter']['queryProjects']['projects']
                for project in projects:
                    if project['key'] == key_project and board.sprintSupportEnabled:
                        boards.append(board)
            except KeyError as e:
                print("\"project\" key not found in the queryProjects' board - Reason: %s" % e)

        return boards

    def get_sprints_by_board_id(self, id_board):
        sprints = self.jira.sprints(id_board, extended=True, maxResults=100)
        return [sprint for sprint in sprints if None != sprints]

    def get_sprint_dates(self, sprint):
        start_date = sprint.raw['startDate']
        end_date = sprint.raw['endDate']
        complete_date = sprint.raw['completeDate']

        dsd = self.ddp.get_date_data(start_date, date_formats=['%d/%b/%y %I:%M %p'])
        dcd = self.ddp.get_date_data(complete_date if 'None' != complete_date else end_date,
                                date_formats=['%d/%b/%y %I:%M %p'])

        return dsd, dcd

    def get_closed_issues_by_sprint(self, sprint_id, start_date, end_date):
        query = "sprint = {0:d} and issuetype not in (Story, Sub-Bug, Sub-Improvement) and status = Done and labels not in (Unplanned)".format(
            sprint_id)
        return self.get_issues_by_jql(query)

    def get_inprogress_issues_by_sprint(self, sprint_id, start_date, end_date):
        query = "sprint = {0:d} and issuetype not in (Story, Sub-Bug, Sub-Improvement) and status != Done and labels not in (Unplanned)".format(
            sprint_id)
        return self.get_issues_by_jql(query)

    def filter_issues_by_label(self, issues, labels):
        items = []
        total = 0.0
        for issue in issues:
            for label in issue.raw['fields']['labels']:
                if label.lower() in [lbl.lower() for lbl in labels]:
                    items.append(issue)
                    total += issue.raw['fields']['customfield_10006']
        return total, items

    def get_issues_by_jql(self, jql):
        issues = self.jira.search_issues(jql, maxResults=500)
        total_points = sum(
            map(lambda s: s.raw['fields']['customfield_10006'] if None != s.raw['fields']['customfield_10006'] else 0.0,
                issues))
        total_test_points, test_issues = self.filter_issues_by_label(issues, ['Test'])
        total_front_points, front_issues = self.filter_issues_by_label(issues, ['Frontend'])
        total_back_issues, back_issues = self.filter_issues_by_label(issues, ['Backend'])
        data = {
            'total_points': float(total_points),
            'issues': issues,
            'total_test_points': total_test_points,
            'test_issues': test_issues,
            'total_front_points': total_front_points,
            'front_issues': front_issues,
            'total_back_points': total_back_issues,
            'back_issues': back_issues
        }
        return data

    def extract_points_series_burned_dayly(self, start_date, end_date, issues, total_points):
        d1 = start_date
        d2 = end_date
        # this will give you a list containing all of the dates
        dd = [d1 + timedelta(days=x) for x in range((d2 - d1).days + 1)]
        average_by_day = round((total_points / len(dd)), 2)
        basic_line = [total_points]
        burned_points_line = [total_points]
        values_burned = [0.0]
        aux = total_points - average_by_day
        burned = total_points
        for actual_date in dd[1:]:
            aux = (aux - average_by_day) if (aux - average_by_day) >= 0.0 else 0.0
            basic_line.append(aux)
            ti_burned_points = self.get_points_burned_by_day(issues, actual_date)
            values_burned.append(ti_burned_points)
            burned = (burned - ti_burned_points) if (burned - ti_burned_points) >= 0.0 else 0.0
            burned_points_line.append(burned)
        # This will convert the dates created by range
        # ddd = [(d1 + timedelta(days=x)).strftime('%d/%m/%Y') for x in range((d2 - d1).days + 1)]
        ddd = [(d1 + timedelta(days=x)) for x in range((d2 - d1).days + 1)]
        return {
            'dates': ddd,
            'basic_line_series': basic_line,
            'burned_line_series': burned_points_line,
            'values_burned': [str(value) for value in values_burned]
        }

    def get_points_burned_by_day(self, issues, actual_date):
        points = 0.0
        for issue in issues:
            # '2017-02-08T07:48:39.000-0200'
            str_date = issue.raw['fields']['updated']
            update_date = self.ddp.get_date_data((str_date.split('T')[0]), date_formats=['%Y-%m-%d'])['date_obj']
            if actual_date.strftime('%d-%m-%Y') == update_date.strftime('%d-%m-%Y'):
                points += issue.raw['fields']['customfield_10006'] if None != issue.raw['fields']['customfield_10006'] else 0.0
        return points
Beispiel #14
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
Beispiel #15
0
class JiraProgramMetrics:
    def __init__(self):
        self._args = self.parse_args()
        self._server = _parse_server(self._args)

        if jamp.JIRA_PASSWORD_ENV not in os.environ:
            raise KeyError(
                f"The environment variable '{jamp.JIRA_PASSWORD_ENV}'"
                f" is required to authenticate with the Jira Server.")

        credential = os.environ[jamp.JIRA_PASSWORD_ENV]

        # JIRA for all normal Jira activity (Boards, Sprints, Issues, etc.)
        self.jira_client = JIRA(server=self._server,
                                basic_auth=(self._args.user, credential),
                                options={'agile_rest_path': 'agile'})

        # JIRA_Reports used only for reporting, not for general Jira access
        self.reports_client = JIRAReports(server=self._server,
                                          basic_auth=(self._args.user,
                                                      credential))

        if self.use_teams:
            # JIRA for all normal Jira activity (Boards, Sprints, Issues, etc.)
            self.teams_client = JIRATeams(
                server=self._server,
                basic_auth=(self._args.user, credential),
                options={'agile_rest_path': 'teams-api'})

        self._map = JiraFieldMapper(self.jira_client._options,
                                    self.jira_client._session)

    def jira_key(self, field_key):
        return self._map.jira_key(field_key)

    @property
    def cfd_requested(self) -> bool:
        return self._args.cfd is not None

    @property
    def cfd_filename(self) -> bool:
        return self._args.cfd

    @property
    def use_teams(self):
        return self._args.teams

    def board_list(self):
        boards = []
        if not self._args.board:
            return self.jira_client.boards(maxResults=None)

        filters = self._args.board.split(';')
        for f in filters:
            tuple = f.split(":")
            if len(tuple) == 1:
                command = 'MATCH_STARTS_WITH'
                pattern = tuple[0]
            elif len(tuple) == 2:
                (pattern, command) = tuple
            else:
                raise ValueError(
                    f"Poorly formed filter {f}. Must be PATTERN:COMMAND, where"
                    f"COMMAND is either 'MATCH_STARTS_WITH' or 'MATCH_EXACT'")

            for b in self.jira_client.boards(type='scrum', maxResults=None):
                print(b.name)
                if "IDAP" in b.name:
                    pprint.pprint(b.raw)
                if command == 'MATCH_STARTS_WITH' and b.name.startswith(
                        pattern):
                    boards.append(b)
                elif command == 'MATCH_EXACT' and b.name == pattern:
                    boards.append(b)

        return boards

    def build_cfd(self) -> List[CfdReport]:
        """
        """
        cfd_list = []
        for board in self.board_list():
            print(board, "hi")
            cfd = self.reports_client.cfd_report(board_id=board.id)
            cfd_list.append(cfd)
        return cfd_list

    def build_report(self) -> pd.DataFrame:
        """
        """

        HEADERS = (
            "Team",
            "Sprint",
            "State",
            "Story Points Committed",
            "Story Ponts Committed VC",
            "Story Points Added",
            "Story Points Removed",
            "Story Points Not Completed",
            "Story Points Completed",
            "Story Points Completed VC",
            "% Complete",
            '# Issues Committed',
            "# Issues Added",
            "# Issues Removed",
            "# Issues Not Completed",
            "# Issues Completed",
            "% Complete",
        )

        data = []
        for board in self.board_list():
            vr = self.reports_client.velocity_report(board_id=board.id)
            print(f"Examining board: {board.name} ({board.id})")
            if board.type != 'scrum':
                continue

            for sprint in self.jira_client.sprints(board_id=board.id,
                                                   maxResults=None):
                try:
                    # Once during a run, Jira returned a internal error
                    # ... HTTPS 500 "Passed List had more than one value."
                    # ... Catch the error and continue on.
                    print(f"Examining sprint: {sprint.name} ({sprint.id})")

                    sr = self.reports_client.sprint_report(board_id=board.id,
                                                           sprint_id=sprint.id)
                except JIRAError as err:
                    print("JIRAError occured: ", err)
                    continue

                if "IDAP" in board.name:
                    pprint.pprint(sr.raw)

                if sr.committed > 0.0:
                    percent_complete = sr.completedIssuesEstimateSum / sr.committed
                else:
                    percent_complete = NAN

                data.append((
                    board.name,
                    sr.sprint.name,
                    sr.sprint.state,
                    sr.committed,
                    vr.committed(sprint.id),
                    sr.added_sum,
                    sr.puntedIssuesEstimateSum,
                    sr.issuesNotCompletedEstimateSum,
                    sr.completedIssuesEstimateSum,
                    vr.completed(sprint.id),
                    percent_complete,
                    sr.committed_count,
                    sr.added_count,
                    sr.puntedIssuesCount,
                    sr.issuesNotCompletedInCurrentSprintCount,
                    sr.completedIssuesCount,
                    sr.percent_complete_count,
                ))

        df = pd.DataFrame.from_records(data, columns=HEADERS)
        return df

    def parse_args(self):
        parser = argparse.ArgumentParser(description='Build Program in Jira')
        # group = parser.add_mutually_exclusive_group()
        parser.add_argument('--user',
                            required=True,
                            type=str,
                            help='User name')
        parser.add_argument(
            '--server',
            required=True,
            type=str,
            help='Jira server address (e.g. https://ale-dev.atlassian.net).'
            'Typically only the server name is required, and no additional'
            'path elements are needed in the URL.')
        parser.add_argument(
            '--force_full_url',
            action='store_true',
            help=
            'Force server parameter to use the full path. Typically the server '
            'name is truncated to include only protocol server name and remove any '
            'extra path elements.')
        parser.add_argument(
            '--cfd',
            type=str,
            help=
            'Create a Cumulative Flow Diagram output file with the specified file name'
        )
        parser.add_argument(
            '--board',
            type=str,
            help='Board name and matching criteria which uses the following '
            'syntax: [<name>:<match>;<str2>:<match2>;...] where match is one of'
            ' MATCH_EXACT | MATCH_STARTS_WITH.  (e.g. --board JAMP:MATCH_EXACT)'
        )
        parser.add_argument(
            '--teams',
            action='store_true',
            help=
            'Test/utilize the teams API (if provisioned on the jira instance)')

        return parser.parse_args()

    def run(self):

        if self.use_teams:
            teams = self.teams_client.teams()
            pprint.pprint(teams)
            for t in teams:
                print(t.title)

        if self.cfd_requested:
            cfd_list = self.build_cfd()
            writer = pd.ExcelWriter(self.cfd_filename, engine='xlsxwriter')
            MAX_TAB_NAME_LENGTH = 30
            board_name_max = MAX_TAB_NAME_LENGTH - len('CFD ')
            for cfd in cfd_list:
                df = cfd.report()
                df.to_excel(
                    writer,
                    sheet_name=f'CFD {cfd.board_name[:board_name_max]}',
                    index=False)

            writer.save()

        else:
            df = self.build_report()

            # Create a Pandas Excel writer using XlsxWriter as the engine.
            writer = pd.ExcelWriter('pandas_simple.xlsx', engine='xlsxwriter')

            # Convert the dataframe to an XlsxWriter Excel object.
            df.to_excel(writer, sheet_name='Sprint Report', index=False)

            # Close the Pandas Excel writer and output the Excel file.
            writer.save()
Beispiel #16
0
class JiraSession(object):
    def __init__(self, server, account, password, verify=True):
        """
        Init Jira Session
        :param server:
        :param account:
        :param password:
        :param verify:
        """
        self.__server = server
        self.__account = account
        self.__password = password
        self.__jira_opts = {
            'server': self.__server,
            'verify': verify,
        }
        self.__session = JIRA(self.__jira_opts,
                              basic_auth=(self.__account, self.__password))

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

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

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

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

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

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

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

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

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

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

    def get_project_versions(self, pid: str):
        """
        Get project versions
        :param pid:
        :return: [<name, id>]
        """
        logging.info(u'Get JIRA Project %s Versions' % pid)
        return self.__session.project_versions(project=pid)
Beispiel #17
0
class JiraBoard(object):
    def __init__(self,
                 config: ConfigParser,
                 userpath: str = None,
                 testMode: bool = False):
        """Initialize JiraBoard with all relevant QA Jira stories.

        :param config: Jira configuration settings
        :param testMode: if True, load test data from a JSON file instead of making requests to the API
        """
        try:
            self.host = config.get('jira', 'url')
            self.username = config.get('jira', 'username')
            self.token = config.get('jira', 'token')
            self.project_key = config.get('jira', 'project_key')
        except ParsingError as e:
            raise JiraBoardException

        # Tester credentials import
        if userpath is not None:
            upath = (os.path.relpath(os.path.join('config', userpath)))
        else:
            upath = (os.path.relpath(os.path.join('config', 'users.ini')))
        self.testers = [
            t['jira_displayname']
            for t in get_configs(['jira_displayname'], upath).values()
        ]
        self.testMode = testMode
        self.options = {'server': self.host}
        self.jira = JIRA(self.options, auth=(self.username, self.token))
        self.board = self.get_board(self.project_key)
        self.current_sprint = self.get_current_sprint(self.board.id)
        self.raw_issues = []
        self.stories = []

    def add_new_filter(self, filter_name: str, new_query: str) -> Filter:
        """Add a new JQL filter to the Jira project.

        :param filter_name: name for the filter
        :param new_query: filter criteria in the form of a JQL string.
        :return:
        """
        try:
            result = self.jira.create_filter(filter_name, jql=new_query)
        except JIRAError as e:
            raise JiraBoardException(
                f'[!] {str(e)}: Failed to get Jira stories.')
        else:
            return result

    def update_filter(self, filter_id: int, new_query: str) -> Filter:
        """Update an existing JQL filter associated with the Jira project.

        :param filter_id: id for the filter
        :param new_query: filter criteria in the form of a JQL string.
        :return:
        """
        try:
            result = self.jira.update_filter(filter_id, jql=new_query)
        except JIRAError as e:
            raise JiraBoardException(
                f'[!] {str(e)}: Failed to update Jira filter.')
        else:
            return result

    def get_project(self) -> Project:
        """Get active Jira project.

        :return: JSON object representing entire Jira project, otherwise None
        """
        try:
            projects = self.jira.projects()
        except JIRAError as e:
            raise JiraBoardException(
                f'[!] {str(e)}: Failed to get Jira projects.')
        else:
            return next(
                filter(lambda proj: proj.key.upper() == self.project_key,
                       projects), None)

    def get_board(self, project_key: str) -> Board:
        """Get active Jira board given a project_key.

        :param project_key: Jira project key
        :return: JSON object representing entire Jira board, otherwise None
        """
        try:
            boards = self.jira.boards()  # (project_key)
        except JIRAError as e:
            raise JiraBoardException(
                f'[!] {str(e)}: Failed to get Jira projects.')
        else:
            return next(
                filter(
                    lambda board: board.name.lower() == 'medhub development',
                    boards), None)

    def get_current_sprint(self, board_id: int) -> Sprint:
        """Given a board_id, get the current sprint the project is in. Also try and filter out that Moodle shit.

        :param board_id: ID for the current Jira board
        :return:
        """
        try:
            sprints = self.jira.sprints(board_id)
        except JIRAError as e:
            raise JiraBoardException(
                f'[!] {str(e)}: Failed to get Jira sprints.')
        else:
            res = next(
                filter(
                    lambda story: story.state.lower() == 'active' and 'yaks' in
                    story.name.lower(), sprints), None)
            if res is None:
                raise JiraBoardException('[!] No current sprint')
            return res

    def get_jql_filter(self, filter_id: int) -> Filter:
        """

        :param filter_id:
        :return:
        """
        try:
            j_filter = self.jira.filter(filter_id)
        except JIRAError as e:
            raise JiraBoardException(
                f'[!] {str(e)}: Failed to get Jira filter.')
        else:
            return j_filter

    def get_issues_from_filter(self, filter_id: int) -> list:
        """Get Jira issues returned by a given JQL filter_id.

        :param filter_id: id for the filter
        :return:
        """
        jql = self.get_jql_filter(filter_id).jql
        try:
            issues = self.jira.search_issues(jql, maxResults=100)
        except JIRAError as e:
            raise JiraBoardException(
                f'[!] {str(e)}: Failed to get issues from Jira filter.')
        else:
            return issues

    def get_issues(self, filter_id: int) -> list:
        """Seems weirdly like a dupe of get_issues_from_filter(), but if things are working I don't wanna break it right now..

        :param filter_id: id for the filter
        :return:
        """
        if self.testMode:
            jsonData = open(os.path.join('testJiraData.json'),
                            'r',
                            encoding='utf-8')
            issues = json.loads(jsonData.read())
        else:
            try:
                issues = self.get_issues_from_filter(filter_id)
            except JIRAError as e:
                raise JiraBoardException(
                    f'[!] {str(e)}: Failed to get issues from Jira filter.')
        return issues

    def get_issue(self, issue_key: str, fields: str = 'status') -> Issue:
        """Get a Jira story given a key.

        :param issue_key: a Jira story key
        :param fields:
        :return:
        """
        try:
            issue = self.jira.issue(issue_key,
                                    fields=fields,
                                    expand='changelog')
        except JIRAError as e:
            raise JiraBoardException(
                f'[!] {str(e)}: Failed to get Jira issue.')
        else:
            if issue is None:
                raise JiraBoardException(
                    f'[!] Unable to retrieve issue {issue_key}')
            return issue

    def get_current_status(self, issue_key: str) -> str or None:
        """Get the current status for a given Jira story.

        :param issue_key: a Jira story key
        :return:
        """
        try:
            issue = self.jira.issue(issue_key,
                                    fields='status',
                                    expand='changelog')
        except JIRAError as e:
            print(
                f'[!] {str(e)}: Failed to get Jira issue.\nRetrying in a few seconds.'
            )
            raise JiraBoardException
        else:
            if issue is not None:
                return issue.fields.status.name
            else:
                return None

    def get_current_status_category(self, issue_key: str) -> str or None:
        """Get the status category for a given Jira story.

        :param issue_key: a Jira story key
        :return:
        """
        try:
            issue = self.jira.issue(issue_key,
                                    fields='status',
                                    expand='changelog')
        except JIRAError as e:
            print(f'[!] {str(e)}: Failed to get Jira issue.')
            raise JiraBoardException
        else:
            if issue is not None:
                return issue.fields.status.statusCategory.name
            else:
                return None

    def get_most_recent_status_change(self, issue_key: str) -> str or None:
        """Get the most recent status change for a Jira story, given it's issue_key.

        :param issue_key: a Jira story key
        :return: object representing the most recent status change
        """
        try:
            issue = self.jira.issue(issue_key,
                                    fields='status',
                                    expand='changelog')
        except JIRAError as e:
            print(f'[!] {str(e)}: Failed to get Jira issue.')
            raise JiraBoardException
        else:
            if issue is not None:
                return next(
                    filter(lambda s: s['toString'], issue.fields.status.name),
                    None)
            else:
                return None

    def is_hotfix(self, issue_key: str) -> bool:
        """Given am issue_key, check if the story is a hotfix.

        :param issue_key: a Jira story key
        :return boolean: True if story is a hotfix, otherwise False
        """
        try:
            issue = self.jira.issue(issue_key,
                                    fields='labels',
                                    expand='changelog')
        except JIRAError as e:
            print(
                f'[!] {str(e)}: Failed to get Jira issue.\nRetrying in a few seconds.'
            )
            raise JiraBoardException
        else:
            if issue is not None:
                return next(
                    filter(lambda l: l.lower() == 'hotfix',
                           issue.fields.labels), None) is not None
            else:
                return False

    def is_in_staging(self, issue_key: str, stories: list) -> bool:
        """Given am issue_key, check if the story is in the staging branch.

        :param issue_key: a Jira story key
        :param stories: a list of Jira stories
        :return boolean: True if story is in staging, otherwise False
        """
        in_staging = False
        issue = next(
            filter(lambda story: story['jira_key'] == issue_key, stories),
            None)
        if issue is not None:
            in_staging = issue['in_staging']
        return in_staging

    def is_fresh_qa_ready(self, issue_key: str) -> bool:
        """Given am issue_key, check if the story is ready for QA for the very first time.

        :param issue_key: a Jira story key
        :return boolean: True if the story has a current status of "Ready for QA Release" and no fail statuses in the past, otherwise False
        """
        current_status = self.get_current_status(issue_key)
        qa_fail = self.has_failed_qa(issue_key)
        if current_status is None:
            raise JiraBoardException('[!] No current status found')
        return current_status == 'Ready for QA Release' and not qa_fail

    def is_stale_qa_ready(self, issue_key: str) -> bool:
        """Given am issue_key, check if the story is ready for QA after previously failing testing.

        :param issue_key: a Jira story key
        :return boolean: True if the story has a current status of 'Ready for QA Release' and at least one prior fail status, otherwise False
        """
        current_status = self.get_current_status(issue_key)
        qa_fail = self.has_failed_qa(issue_key)
        if current_status is None:
            raise JiraBoardException('[!] No current status found')
        return current_status == 'Ready for QA Release' and qa_fail

    def is_in_qa_testing(self, issue_key: str) -> bool:
        """Given am issue_key, check if the story is currently in 'QA Testing'.

        :param issue_key: a Jira story key
        :return boolean: True if the story has a current status of 'QA Testing', otherwise False
        """
        current_status = self.get_current_status(issue_key)
        if current_status is None:
            raise JiraBoardException('[!] No current status found')
        return current_status == 'QA Testing'

    def has_complete_status(self, issue_key: str) -> bool:
        """Given am issue_key, check if the story has a 'Complete' status category.

        :param issue_key: a Jira story key
        :return boolean: True if the story has a current status category of 'Done', otherwise False
        """
        current_status_category = self.get_current_status_category(issue_key)
        if current_status_category is None:
            raise JiraBoardException('[!] No current status category found')
        return current_status_category == 'Done'

    def for_qa_team(self, issue_key: str) -> bool:
        """Similar to passed_qa but checks if a QA tester moved this at any point to make sure it actually got tested.

        :param issue_key: a Jira story key
        :return boolean: True if the story has a status change with a valid QA tester, otherwise False
        """
        try:
            issue = self.jira.issue(issue_key,
                                    fields='status',
                                    expand='changelog')
        except JIRAError as e:
            raise JiraBoardException(
                f'[!] {str(e)}: Failed to get Jira issue.')
        else:
            if issue is None:
                raise JiraBoardException('[!] NoneType issue is not valid.')
            else:
                # Let's try this another way
                statuses = self.get_statuses(issue.changelog.histories)
                tester = next(
                    filter(
                        lambda s: s['authorName'] in
                        self.testers and 'qa release' in s['fromString'].lower(
                        ) and 'qa testing' in s['toString'].lower(), statuses),
                    None)
                return tester is not None

    def passed_qa(self, issue_key: str) -> bool:
        """Given an issue_key, check if the story has passed QA.

        :param issue_key: a Jira story key
        :return boolean: True if the story has a passing status with a valid QA tester, otherwise False
        """
        try:
            issue = self.jira.issue(issue_key,
                                    fields='status',
                                    expand='changelog')
        except JIRAError as e:
            raise JiraBoardException(
                f'[!] {str(e)}: Failed to get Jira issue.')
        else:
            if issue is None:
                raise JiraBoardException('[!] NoneType issue is not valid.')
            else:
                statuses = self.get_statuses(issue.changelog.histories)
                tester = next(
                    filter(lambda s: s['authorName'] in self.testers,
                           statuses), None)
                return self.has_complete_status(
                    issue.key) and tester is not None

    def has_failed_qa(self, issue_key: str) -> bool:
        """Given an issue_key, check if the story has failed QA in the past.

        :param issue_key: a Jira story key
        :return boolean: True if the story has a fail status in the past, otherwise False
        """
        try:
            issue = self.jira.issue(issue_key,
                                    fields='status',
                                    expand='changelog')
        except JIRAError as e:
            raise JiraBoardException(
                f'[!] {str(e)}: Failed to get Jira issue.')
        else:
            if issue is None:
                raise JiraBoardException('[!] NoneType issue is not valid.')
            else:
                statuses = self.get_statuses(issue.changelog.histories)
                failStatus = next(
                    filter(
                        lambda status: (status['authorName'] in self.testers)
                        and status['fromString'] == 'QA Testing' and status[
                            'toString'] == 'In Progress', statuses), None)
                return True if failStatus is not None else False

    def is_currently_failed(self, issue_key: str) -> bool:
        """Given an issue_key, check if a Jira story currently has a failure status.

        :param issue_key: a Jira story key
        :return boolean: True if the story has a current fail status, otherwise False
        """
        current_status = self.get_current_status(issue_key)
        if current_status is None:
            raise JiraBoardException('[!] No current status found')
        return current_status in ['In Progress', 'Backlog']

    def is_defect(self, issue_key: str) -> bool:
        """Return True if Jira issuetype is a defect.

        :param issue_key:
        :return:
        """
        return self.get_issue(
            issue_key,
            fields='issuetype').fields.issuetype.name.lower() == 'defect'

    def is_qa_task(self, issue_key: str) -> bool:
        """Return True if Jira issuetype is a defect.

        :param issue_key:
        :return:
        """
        return self.get_issue(
            issue_key,
            fields='issuetype').fields.issuetype.name.lower() == 'qa task'

    def is_bug(self, issue_key: str) -> bool:
        """Return True if Jira issuetype is a bug.

        :param issue_key:
        :return:
        """
        return self.get_issue(
            issue_key,
            fields='issuetype').fields.issuetype.name.lower() == 'bug'

    def get_statuses(self, change_log: dict or list) -> list:
        """Get all status changes from a change_log associated with a Jira story.

        :param change_log: expanded list of fields returned from Jira API for a single issue
        :return list: a list of dicts representing the status change and it's author
        """
        return [
            dict(authorName=ch.author.displayName,
                 created=ch.created,
                 fromString=ch.items[0].fromString,
                 toString=ch.items[0].toString) for ch in change_log
            if ch.items[0].field.lower() == 'status'
        ]

    def get_subtasks(self, issue_key: str) -> list:
        """Get subtasks of a Jira story.

        :param issue_key: a Jira story key
        :return list: a list of subtask names
        """
        try:
            issue = self.jira.issue(issue_key,
                                    fields='status',
                                    expand='changelog')
        except JIRAError as e:
            raise JiraBoardException(
                f'[!] {str(e)}: Failed to get Jira issue.')
        else:
            if issue is None:
                raise JiraBoardException('[!] NoneType issue is not valid.')
            else:
                return [task.lower() for task in issue.fields.subtasks]

    def get_attachments(self, issue_key: str) -> list:
        """Get attachments from a Jira story.

        :param issue_key: a Jira story key
        :return list: a list of URLs pointing to story attachments
        """
        try:
            issue = self.jira.issue(issue_key,
                                    fields='attachment',
                                    expand='changelog')
        except JIRAError as e:
            raise JiraBoardException(
                f'[!] {str(e)}: Failed to get Jira issue.')
        else:
            if issue is None:
                raise JiraBoardException('[!] NoneType issue is not valid.')
            else:
                return [
                    '{}secure/attachment/{}/{}'.format(self.host, a.id,
                                                       a.filename)
                    for a in issue.fields.attachment
                ]

    def get_labels(self, issue_key: str) -> list:
        """Get a list of labels attached to a story by issue_key.

        :param issue_key: a Jira story key
        :return list: a list Jira story labels
        """
        try:
            issue = self.jira.issue(issue_key,
                                    fields='labels',
                                    expand='changelog')
        except JIRAError as e:
            raise JiraBoardException(
                f'[!] {str(e)}: Failed to get Jira issue.')
        else:
            if issue is None:
                raise JiraBoardException('[!] NoneType issue is not valid.')
            else:
                return [label.lower() for label in issue.fields.labels]

    def get_parsed_stories(self,
                           raw_issues: list,
                           testrail_mode: bool = False) -> list:
        """Given a collection of raw Jira stories, parses them down to JSON objects containing needed fields only.

        :param raw_issues: JSON collection of stories returned from the Jira API
        :param testrail_mode: Should QA dates be ignored? If they're in the release but haven't been QAed yet... yes.
        :return parsed_stories: a list of parsed stories ready for one of the reconcile methods
        """
        parsed_stories = []

        for issue in raw_issues:
            _story = self.get_issue(
                issue.key,
                fields=
                'summary,description,comment,labels,created,updated,status')
            _testedBy = 'unassigned'
            _hasFailed = False
            _currentStatus = self.get_current_status(_story.key)
            _changelog = sorted(_story.changelog.histories,
                                key=lambda ch: ch.created,
                                reverse=True)
            _statuses = self.get_statuses(_changelog)
            _validTester = next(
                filter(lambda status: status['authorName'] in self.testers,
                       _statuses), None)
            _testedBy = _validTester[
                'authorName'] if _validTester is not None else 'unassigned'
            _url = urljoin(self.host, 'browse/{}'.format(_story.key))
            _labels = self.get_labels(_story.key)

            if _story.fields.summary is not None:
                _summary = ''.join(
                    re.findall(r'[^*`#\t\'"]', _story.fields.summary))
            else:
                _summary = ''

            if _story.fields.description is not None:
                _desc = ''.join(
                    re.findall(r'[^*`#\t\'"]', _story.fields.description))
            else:
                _desc = ''

            if testrail_mode:
                record = dict(
                    jira_key=_story.key,
                    jira_url=_url,
                    jira_summary=_summary,
                    jira_desc=_desc,
                    labels=_labels,
                    jira_created=_story.fields.created,
                    jira_updated=_story.fields.updated,
                    tested_by=_testedBy,
                )

            else:
                _qaStatuses = list(
                    filter(
                        lambda status: status['fromString'] == 'QA Testing' or
                        status['toString'] == 'QA Testing', _statuses))
                _validTester = next(
                    filter(lambda status: status['authorName'] in self.testers,
                           _statuses), None)

                _qaDateStatus = next(
                    filter(
                        lambda status: status['fromString'] == 'In Progress'
                        and status['toString'] == 'Ready for QA Release' and
                        _story.key not in parsed_stories, _statuses), None)

                if _qaDateStatus is None:
                    if self.is_defect(_story.key) or self.is_qa_task(
                            _story.key):
                        # defects or QA tasks don't go on the board
                        continue

                    print(
                        f'[!] QA date not found on {str(issue.key)}\n\tEither the issue type did not get excluded or a developer may have accidentally moved the story into QA'
                    )
                    _movedToQaDate = dateutil.parser.parse(
                        _story.fields.updated)
                else:
                    _movedToQaDate = dateutil.parser.parse(
                        _qaDateStatus['created'])

                _hasFailed = True if len(_qaStatuses) > 0 else False

                _testedBy = _validTester[
                    'authorName'] if _validTester is not None else 'unassigned'

                _comments = '\n'.join([
                    '\n_**{}** at {}_:\n\n{}\n\n'.format(
                        comment.author,
                        dateutil.parser.parse(
                            comment.updated).strftime('%Y-%m-%d %H:%M'),
                        ''.join(re.findall(r'[^*`#\t\'"]', comment.body)))
                    for comment in sorted(_story.fields.comment.comments,
                                          key=lambda s: s.updated,
                                          reverse=True)
                ])

                _statuses = '\n'.join([
                    '_**{}**_:\n\nFrom {} to {} at {}\n\n'.format(
                        s['authorName'], s['fromString'], s['toString'],
                        dateutil.parser.parse(
                            s['created']).strftime('%Y-%m-%d %H:%M'))
                    for s in _statuses
                ])

                if _movedToQaDate is not None:
                    _qaDate = _movedToQaDate.strftime('%Y-%m-%d %H:%M:%S%z')
                else:
                    raise JiraBoardException('[!] No QA date status found')

                _api_url = urljoin(self.host,
                                   'rest/api/2/issue/{}'.format(_story.key))
                _hotfix = self.is_hotfix(_story.key)
                _inStaging = False
                _attachments = self.get_attachments(_story.key)

                record = dict(
                    jira_id=issue.id,
                    jira_key=_story.key,
                    jira_url=_url,
                    jira_api_url=_api_url,
                    jira_summary=_summary,
                    jira_desc=_desc,
                    jira_created=_story.fields.created,
                    jira_updated=_story.fields.updated,
                    jira_qa_date=_qaDate,
                    tested_by=_testedBy,
                    current_status=_currentStatus,
                    has_failed=_hasFailed,
                    in_staging=_inStaging,
                    is_hotfix=_hotfix,
                    comments=_comments,
                    statuses=_statuses,
                    labels=_labels,
                    attachments=_attachments,
                    last_known_commit_date=None,
                    git_commit_message=None,
                )

            parsed_stories.append(record)

        if testrail_mode:
            result = sorted(parsed_stories,
                            key=lambda story: story['jira_updated'])
        else:
            result = sorted(parsed_stories,
                            key=lambda story: story['jira_qa_date'])
        return result
Beispiel #18
0
from jira import JIRA
import datetime
from dateutil import parser
import yaml

with open('jira.yaml') as f:
    details = yaml.load(f)
    address = details['address']
    user = details['credentials']['user']
    password = details['credentials']['password']

# You need to set a password, or this can be changed to use oauth apparently
jira = JIRA(address, basic_auth=(user, password))

board = next(board for board in jira.boards() if board.name == u'Enterprise Release')

sprint = next(
    sprint for sprint in jira.sprints(id=board.id) if
    sprint.state == u'ACTIVE' and
    sprint.name.startswith('Release Squad Sprint')
)

jql = 'sprint = {sprint_id}'.format(sprint_id=sprint.id)
issue_search_results = jira.search_issues(jql)

issues = set(jira.issue(issue.key, expand='changelog') for issue in issue_search_results)

issues_moved_into_sprint = []

for issue in issues:
Beispiel #19
0
class JiraDataStructuresManager():
    ''' Extracts structural data from Jira for further analysis '''
    options = None
    jira = None
    cfg = None

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

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

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

        for board in boards:

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

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

            except Exception:
                pass

        return csv

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

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

        csv = "SprintName;State\n"

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

        return csv

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

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

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

        return csv

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

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

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

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

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

        fileManager.getSendToCSVFile(stringBuffer.getvalue())

        if (name != None):
            try:
                result = nameMap[name]
            except:
                return None
            return result
        else:
            return None
Beispiel #20
0
class jira_handler:
    def __init__(self, project_name):
        self.mongo_db = mongodb_class.mongoDB()
        self.jira = JIRA('http://172.16.60.13:8080',
                         basic_auth=('shenwei', 'sw64419'))
        self.gh = GreenHopper({'server': 'http://172.16.60.13:8080'},
                              basic_auth=('shenwei', 'sw64419'))
        self.name = project_name
        self.project = self.jira.project(self.name)
        self.pj_name = u"%s" % self.project.name
        self.pj_manager = u"%s" % self.project.lead.displayName
        """获取项目版本信息
        """
        _versions = self.jira.project_versions(self.name)
        self.version = {}
        for _v in _versions:
            _key = (u"%s" % _v).replace('.', '^')
            if not self.version.has_key(_key):
                self.version[_key] = {}
            self.version[_key][u"id"] = _v.id
            self.version[_key]['startDate'] = ""
            self.version[_key]['releaseDate'] = ""
            if 'startDate' in dir(_v):
                self.version[_key]['startDate'] = _v.startDate
            if 'releaseDate' in dir(_v):
                self.version[_key]['releaseDate'] = _v.releaseDate
            if self.mongo_db.get_count("project", {"version": _key}) > 0:
                self.mongo_db.handler(
                    "project", "update", {"version": _key},
                    dict({"version": _key}, **self.version[_key]))
            else:
                _val = dict({"version": _key}, **self.version[_key])
                print _val
                self.mongo_db.handler("project", "insert", _val)
        self.issue = None

    def _get_board(self):
        _boards = self.jira.boards()
        for _b in _boards:
            if self.name in _b.name:
                return _b.id
        return None

    def get_current_sprint(self):
        """
        获取本阶段sprint名称
        :return: 返回状态为ACTIVE的sprint的名称
        """
        _b_id = self._get_board()
        if type(_b_id) is not types.NoneType:
            _sprints = self.jira.sprints(_b_id)
            for _s in _sprints:
                if _s.state == 'ACTIVE':
                    return _s.name
        return None

    def get_sprint(self):
        if "customfield_10501" in self.issue.raw['fields'] and \
                type(self.issue.fields.customfield_10501) is not types.NoneType:
            return u'%s' % self.issue.fields.customfield_10501[0].split(
                'name=')[1].split(',')[0]
        return None

    def get_versions(self):
        _v = {}
        for _k in self.version:
            _key = (u"%s" % _k).replace('^', '.')
            _v[_key] = self.version[_k]
        return _v

    def get_pj_info(self):
        return {'pj_name': self.pj_name, 'pj_manager': self.pj_manager}

    def set_issue_by_name(self, issue_id):
        self.issue = self.jira.issue(issue_id)

    def print_green_hopper(self):
        _f = self.gh.fields()
        for __f in _f:
            __cns = __f['clauseNames']
            print('-' * 8)
            for _n in __cns:
                print u"name: %s" % _n
            print "id: ", u"%s" % __f['id']
            print "name: ", u"%s" % __f['name']

    def get_story_point(self):
        """
        获取Issue(story)的预置成本, 1 point = 4 hours
        :return: 预置成本
        """
        if "customfield_10304" in self.issue.raw['fields'] and \
                type(self.issue.fields.customfield_10304) is not types.NoneType:
            return self.issue.fields.customfield_10304
        return None

    def get_task_time(self):
        return {
            "agg_time": self.issue.fields.aggregatetimeestimate,
            "org_time": self.issue.fields.timeoriginalestimate,
            "spent_time": self.issue.fields.timespent
        }

    def get_landmark(self):
        if len(self.issue.fields.fixVersions) > 0:
            return u"%s" % self.issue.fields.fixVersions[0]
        if len(self.issue.fields.versions) > 0:
            print self.show_name(
            ), " version: %s" % self.issue.fields.versions[0]
            return u"%s" % self.issue.fields.versions[0]
        return ""

    def get_desc(self):
        return u"%s" % self.issue.fields.summary

    def show_name(self):
        return u"%s" % str(self.issue)

    def get_type(self):
        return u"%s" % self.issue.fields.issuetype

    def get_status(self):
        return u"%s" % self.issue.fields.status

    def get_subtasks(self):
        """
        收集issue的相关子任务的issue
        :return: 相关issue字典
        """
        link = {}
        if not link.has_key(self.show_name()):
            link[self.show_name()] = []
        _task_issues = self.issue.fields.subtasks
        for _t in _task_issues:
            link[self.show_name()].append(u"%s" % _t)
        return link

    def get_child_requirement(self):

        link = []
        jql = "issue in  childrenOfParentRequirement('%s')" % self.show_name()
        # print jql
        tot = 0
        while True:
            issues = self.jira.search_issues(jql, maxResults=100, startAt=tot)
            for issue in issues:
                link.append(issue.key)
            if len(issues) == 100:
                tot += 100
            else:
                break
        return link

    def get_link(self):
        """
        收集issue的相关issue
        :return: 相关issue字典
        """
        link = {}
        if self.show_name() not in link:
            link[self.show_name()] = []
        """兼容以前: 与story相关的task是通过issulelinks关联的"""
        _task_issues = self.issue.fields.issuelinks
        for _t in _task_issues:
            if "outwardIssue" in dir(_t):
                """该story相关的任务"""
                link[self.show_name()].append(u"%s" % _t.outwardIssue)
            if "inwardIssue" in dir(_t):
                """该story相关的任务"""
                link[self.show_name()].append(u"%s" % _t.inwardIssue)
        """采用synapseRT插件后对需求的管理"""
        _task_issues = self.get_child_requirement()
        for _t in _task_issues:
            link[self.show_name()].append(_t)

        return link

    def show_issue(self):
        """
        显示issue信息
        :return:
        """
        print("[%s]-%s" % (self.show_name(), self.get_desc())),
        print u"类型:%s" % self.get_type(),
        print(u'状态:%s' % self.get_status()),

        print u"里程碑:%s" % self.get_landmark(),
        _time = self.get_task_time()
        """
        if type(_time['agg_time']) is types.NoneType:
            _time['agg_time'] = ""
        if type(_time['org_time']) is types.NoneType:
            _time['org_time'] = ""
        if type(_time["spent_time"]) is types.NoneType:
            _time["spent_time"] = ""
        """
        if "customfield_11300" in self.issue.raw['fields'] and \
                type(self.issue.fields.customfield_11300) is not types.NoneType:
            _epic_link = self.issue.raw['fields']["customfield_11300"]
        else:
            _epic_link = None
        _issue = {
            u"%s" % self.show_name(): {
                "issue_type": self.get_type(),
                "created": self.issue.fields.created,
                "updated": self.issue.fields.updated,
                "lastViewed": self.issue.fields.lastViewed,
                "users": self.get_users(),
                "status": self.get_status(),
                "landmark": self.get_landmark(),
                "point": self.get_story_point(),
                "agg_time": _time['agg_time'],
                "org_time": _time['org_time'],
                "summary": self.issue.fields.summary,
                "spent_time": _time['spent_time'],
                "sprint": self.get_sprint(),
                "epic_link": _epic_link
            }
        }
        _key = u"%s" % self.show_name()
        if self.mongo_db.get_count("issue", {"issue": _key}) > 0:
            self.mongo_db.handler("issue", "update", {"issue": _key},
                                  dict({"issue": _key}, **_issue[_key]))
        else:
            self.mongo_db.handler("issue", "insert",
                                  dict({"issue": _key}, **_issue[_key]))
        if self.mongo_db.get_count("issue_link", {"issue": _key}) > 0:
            self.mongo_db.handler("issue_link", "update", {"issue": _key},
                                  dict({"issue": _key}, **self.get_link()))
        else:
            self.mongo_db.handler("issue_link", "insert",
                                  dict({"issue": _key}, **self.get_link()))

        return _issue

    def get_users(self):
        """
        获取访问issue的用户
        2018.3.1:改为 经办人 assignee
        :return:
        watcher = self.jira.watchers(self.issue)
        _user = u"%s" % (', '.join(watcher.displayName for watcher in watcher.watchers))
        """
        if type(self.issue.raw['fields']["assignee"]) is types.NoneType:
            return None
        return (u"%s" %
                self.issue.raw['fields']["assignee"]['displayName']).replace(
                    ' ', '')

    def write_log(self, info):
        self.mongo_db.handler("log", "insert", info)

    def write_worklog(self, info):
        _search = {
            'issue': info['issue'],
            'author': info['author'],
            'updated': info['updated']
        }
        self.mongo_db.handler('worklog', 'update', _searchinfo)

    def sync_worklog(self):
        worklogs = self.jira.worklogs(self.show_name())
        wl = {}
        for worklog in worklogs:
            wl['issue'] = self.show_name()
            wl['author'] = u'%s' % worklog.author
            wl['comment'] = u'%s' % worklog.comment
            wl['timeSpent'] = worklog.timeSpent
            wl['timeSpentSeconds'] = worklog.timeSpentSeconds
            wl['updated'] = worklog.updated
            self.write_worklog(wl)

    def scan_issue(self, bg_date, keys, version):
        """
        扫描project收集相关版本的issue信息
        :param bg_date: 起始日期,如 2018-1-31
        :param keys: 关键字,[u'story', u'故事']
        :param version: 版本,里程碑
        :return: 按issue类型进行统计值kv_sum,issue链kv_link,相关任务链task_link
        """
        jql_sql = u'project=%s AND created >= %s ORDER BY created DESC' % (
            self.name, bg_date)
        total = 0
        kv_sum = {}
        kv_link = {}
        task_link = {}

        while True:
            issues = self.jira.search_issues(jql_sql,
                                             maxResults=100,
                                             startAt=total)
            for issue in issues:

                if (u"%s" % issue.fields.issuetype) not in keys:
                    continue

                self.issue = issue

                if ((u"%s" % issue.fields.issuetype) == 'story' and
                        (self.get_landmark() == version or u'入侵' in self.issue.fields.summary)) or \
                        (u"%s" % issue.fields.issuetype) in [u'epic', u'improvement',
                                                             u'New Feature', u'改进', u'新功能']:
                    """收集story相关的任务"""
                    task_link.update(self.get_link())

                    _type = self.get_type()
                    _status = self.get_status()
                    if not kv_sum.has_key(_type):
                        kv_sum[_type] = 0
                        kv_link[_type] = {}

                    kv_sum[_type] += 1
                    if not (kv_link[_type]).has_key(_status):
                        (kv_link[_type])[_status] = []
                    (kv_link[_type])[_status].append(self.show_name())

            if len(issues) == 100:
                total += 100
            else:
                break
        return kv_sum, kv_link, task_link