# find comments with estimations out of pre-planning # zisco 2021-09-30 19:50 from multiprocessing.sharedctypes import Value from types import ClassMethodDescriptorType from jira import JIRA, Issue, Comment, exceptions import re import csv import xlsxwriter import logging import yaml from bisect import bisect_left from datetime import date def take_closest(myList, myNumber): """ Assumes myList is sorted. Returns closest value to myNumber. If two numbers are equally close, return the smallest number. """ pos = bisect_left(myList, myNumber) if pos == 0: return myList[0] if pos == len(myList): return myList[-1] before = myList[pos - 1] after = myList[pos] if after - myNumber < myNumber - before: return after else: return before def get_assignee(type, issue): # isBuergerfrontend = False if (type == 'Dev Backend' or type.startswith('Dev Unit')): # use assignee of story return cfg['def_assignees']['backend'] #'rab9001' #return issue.fields.assignee.name elif type == 'Dev Frontend': # for component in issue.fields.components: # isBuergerfrontend = component.name == "Bürgerfrontend" # if isBuergerfrontend: return cfg['def_assignees']['frontend'] #'thh9001' elif type == 'Dev Citizen-Frontend': return cfg['def_assignees']['cit_frontend'] #'reu9001' elif type.startswith('Test-Anpassung') or type == 'DoD Test': if issue.key.startswith('WRGWR-'): return cfg['def_assignees']['wgwr_test'] #'ada9001' elif issue.key.startswith('BAUWB-'): return cfg['def_assignees']['bauwb_test'] #'osm9005' elif issue.key.startswith('DKARB-'): return cfg['def_assignees']['far_test'] #'osm9005' else: return cfg['def_assignees']['default'] #'zim9003' elif type == 'DoD PO': return cfg['def_assignees']['po'] #'kom9010' elif type == 'DoD SA': return cfg['def_assignees']['sa'] #'krp9002' def create_subtask(issue, parent, type, subtask_sp=0, estimation_remark="", no_unit_test=False): escaped = issue.fields.summary.replace('"', r'\"') logging.info(f'summary ~ \"{escaped}\"') assignee = get_assignee(type, issue) description = get_description(type, no_unit_test=no_unit_test) try: project = parent.split('-')[0] subtask = jira_server.search_issues(f'summary ~ \"{type} {escaped}\" AND project = {project}') # add the project if subtask: single_issue = jira_server.issue(subtask[0].key) if single_issue.fields.summary == f'[{type}] {escaped}': raise exceptions.JIRAError except exceptions.JIRAError: # correct estimation #subtask.fields('timetracking': {'originalEstimate'}).Value() = subtask_sp #single_issue = jira_server.issue(subtask[0].key) try: if cfg['update_desc_only']: single_issue.update(fields={'description': description}) else: single_issue.update(fields={'timetracking': {'originalEstimate': '{}d'.format(subtask_sp)}, 'description': description, 'assignee': {'name': assignee}}) except Exception as e: logging.info(e) logging.info(f'Skip creating Sub-Task {type} for {issue} as it already exists') return issue_type = 'Sub-task' if estimation_remark != "": description += "\nEstimation remark: {}".format(estimation_remark) issue_dict = { 'project': {'id': issue.fields.project.id}, 'summary': "[{}] {}".format(type, escaped), 'description': description, # issue.fields.description, 'issuetype': {'name': issue_type}, 'priority': {'name': issue.fields.priority.name}, 'parent': {'key': parent}, 'labels': issue.fields.labels, #'customfield_10022' - Story Points 'timetracking': {'originalEstimate': "{}d".format(subtask_sp)}, 'assignee': {'name': assignee} } new_issue = jira_server.create_issue(fields=issue_dict) # consider components existingComponents = [] for component in issue.fields.components: existingComponents.append({"name" : component.name}) new_issue.update(fields={"components": existingComponents}) #new_issue.update(priority={"name": issue.fields.priority.name}) logging.info('Created Sub-Task {}'.format(new_issue)) def get_description(type, no_unit_test=False): if (type == 'Dev Backend'): description = cfg['templates']['subtask_desc_be_dev'] if no_unit_test==False: description += cfg['templates']['add_unit_test_desc'] elif (type in ('Dev Frontend', 'Dev Citizen-Frontend')): description = cfg['templates']['subtask_desc_fe_dev'] elif (type == 'DoD SA'): description = cfg['templates']['subtask_desc_sa'] #TODO: Add links to DEV-SubTasks elif (type == 'Test-Anpassung (TOSCA/xRay)'): #TODO: Use desc of story description = 's. Story' #description = cfg['templates']['subtask_desc_test'] elif (type == 'DoD PO'): description = cfg['templates']['subtask_desc_po'] else: description = 's. Story' return description logging.basicConfig(filename='sub_task_generator.log', filemode='a', format='%(asctime)s - %(levelname)s: %(message)s', level=logging.INFO, encoding='utf-8') with open("config.yaml", encoding='utf8') as f: cfg = yaml.load(f, Loader=yaml.FullLoader) host = "https://jira.wien.gv.at/" pat = "OTAyMTM3MjY0MjE0Ovz6bu0ti1TlEdSegmmiWiJ2wGeD" sprint = 5 headers = JIRA.DEFAULT_OPTIONS["headers"].copy() headers["Authorization"] = f"Bearer {pat}" jira_server = JIRA(server=host, options={"headers": headers}) # use filter or single issue issues_Jql = jira_server.search_issues('filter=25827') #, expand='changelog') #issues_Jql = jira_server.search_issues('filter=30617') #, expand='changelog') issues_Jql = jira_server.search_issues('key in (BAUWB-600)') #, expand='changelog') #issues_Jql = jira_server.search_issues('labels in (Refinement11012022) AND type not in (Bug) AND status not in (Done) and ("Story Points" is EMPTY or "Story Points" = 0)') #, expand='changelog') logging.info(f'Creating sub-tasks out of estimation of pre-planning of {date.today()}') xsp = lambda s: s or 0 issue: Issue for issue in issues_Jql: logging.info('Working on {}'.format(issue)) sp_sum = 0 # only Issues with empty (or 0) story-points sp = 0 no_unit_test = False if hasattr(issue.fields, 'customfield_10022'): sp = xsp(issue.fields.customfield_10022) # if sp == 0: # logging.info('Ignoring issue {} as there are Story-Points already omitted'.format(issue)) # else: single_issue = jira_server.issue(issue.key) comments = single_issue.fields.comment.comments # Find all comments made by me on this issue. for comment in comments: comment_lines = comment.body.splitlines() if len(comment_lines) in (5, 9): if comment_lines[0][:5] == "Dev B": if "Dev Unit-Tests: NN" in comment_lines: no_unit_test = True for comment_line in comment_lines: if comment_line != "": # pos = comment_lines[2].find('NN') comment_line_parts = comment_line.split(':') # if 1. Test* != NN create a sub-task (if not existing) if len(comment_line_parts) == 2: if comment_line_parts[1] != u'\xa0': # empty string if (comment_line_parts[1][-2:] != 'NN'): # NN means NotNecessary, "-"" means we don't know yet (but subtask will be created) if (comment_line_parts[1][-1:] == '-'): subtask_ph = 0 estimation_remark = "(not estimated yet)" else: comment_line_sub_parts = comment_line_parts[1].split(' (') if len(comment_line_sub_parts)>1: if comment_line_sub_parts[0].strip().isnumeric(): subtask_ph = float(comment_line_sub_parts[0]) * 0.5 estimation_remark = comment_line_sub_parts[1] else: if comment_line_sub_parts[0].strip().isnumeric(): subtask_ph = float(comment_line_sub_parts[0]) * 0.5 estimation_remark = "" else: if comment_line_sub_parts[0].strip().lower() == "tbd": subtask_ph = take_closest([1,2,3,5,8,13,21,42], sp_sum/3) #if comment_line_parts[0][:15] != "Test-Ausführung": create_subtask(issue, issue.key, comment_line_parts[0], subtask_ph, estimation_remark, no_unit_test=no_unit_test) sp_sum += subtask_ph / 0.5 else: logging.info(f'Sub-task creation skipped {comment_line_parts[0]} equals \'NN\'') # find corresponding sub-task # issuelinks.inwardIssue try: if sp != sp_sum: if cfg['update_sp_sum']: issue.update(fields={'customfield_10022': sp_sum}) logging.info("Estimated story-points changed to Sum(Sub-Tasks story-points): {}".format(sp_sum, sp_sum)) except exceptions.JIRAError: logging.error("Issue {} has no (visible) Story Points-attribute".format(issue.key)) finally: logging.info('Done {}'.format(issue.key)) #create standard sub-tasks if cfg['separate_dod_subtasks']: for type in ['DoD SA','DoD PO']: create_subtask(issue, issue.key, type, no_unit_test=no_unit_test ) #add or remove label #Unit-Test on story-level if no_unit_test == False: #issue.remove_field_value("labels", u'unit_test') issue.add_field_value("labels", u'unit_test') #issue.update(fields={"labels": issue.fields.labels}) #print('{}: {}, SP: {}'.format(issue.key, issue.fields.summary, sp)) #print('Summe {}: {}'.format(assignee, sp_sum)) #print(issues_Jql) # changelog = issues_Jql. # count = 0 # for history in changelog.histories: # for item in history.items: # if item.field == 'status': # if item.toString == "Reopened": # count = count + 1 # print(count)