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 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_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 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 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 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 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_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 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_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 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_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 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 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 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 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 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 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_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_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