|
| 1 | +#!/usr/bin/env python |
| 2 | +# -*- coding: utf-8 -*- |
| 3 | +PROG_VERSION = u"Time-stamp: <2016-05-13 14:00:47 karl.voit>" |
| 4 | + |
| 5 | +import sys |
| 6 | +import os |
| 7 | +import re |
| 8 | +import argparse |
| 9 | +import time |
| 10 | + |
| 11 | +CONFIGDIR = os.getcwdu() |
| 12 | +CONFIGFILEBASENAME = 'jiraconfig' |
| 13 | +CONFIGFILENAME = os.path.join(CONFIGDIR, CONFIGFILEBASENAME) |
| 14 | +CONFIGTEMPLATE = ''' |
| 15 | +JIRA_USER = 'joe' |
| 16 | +JIRA_PASSWORD = 'secret password for me' |
| 17 | +''' |
| 18 | + |
| 19 | +DESCRIPTION = u'''This tool retrieves a Jira issue and returns an Org-mode |
| 20 | +representation according to the system of Karl Voit. |
| 21 | +
|
| 22 | +The output is highly specific for my personal usage. If you want to have |
| 23 | +a similar functionality, you have to adapt it to your needs. This would |
| 24 | +require at least a search&replace of "IPD" with the Jira project ID of |
| 25 | +your choice, all Jira URLs, and the custom org-mode link "ipd:1234". |
| 26 | +''' |
| 27 | + |
| 28 | +EPILOG=u'''autor: Karl Voit <tools@Karl-Voit.at> |
| 29 | +license: GPL v3 or any later version |
| 30 | +URL: https://github.com/novoid/FIXXME/ |
| 31 | +bugreports: via GitHub |
| 32 | +version: ''' + PROG_VERSION + '\n' |
| 33 | + |
| 34 | +parser = argparse.ArgumentParser(description=DESCRIPTION, |
| 35 | + epilog=EPILOG, |
| 36 | + formatter_class=argparse.RawDescriptionHelpFormatter) |
| 37 | + |
| 38 | +parser.add_argument('IPD', nargs='+', help='One or many IPD numbers of stories or epics (without \"IPD-\" prefix)') |
| 39 | + |
| 40 | +verbosity_group = parser.add_mutually_exclusive_group() |
| 41 | +##verbosity_group.add_argument("--verbose", action="store_true") |
| 42 | +##verbosity_group.add_argument("--quiet", action="store_true") |
| 43 | +verbosity_group.add_argument('--version', action='version', version=PROG_VERSION) |
| 44 | + |
| 45 | +args = parser.parse_args() |
| 46 | + |
| 47 | +output_text = u'' # global output string for all stdout/file output operations |
| 48 | + |
| 49 | +def print_line(text): |
| 50 | + global output_text |
| 51 | + output_text += text + '\n' |
| 52 | + print text |
| 53 | + |
| 54 | +def print_item(ipd, message, level=1, empty_checkbox=False, filled_checkbox=False, omit_print=False): |
| 55 | + currentprefix = OUTPUTPREFIX + (level - 1) * u' ' |
| 56 | + text = currentprefix + '- ' |
| 57 | + if empty_checkbox: |
| 58 | + text += '[ ] ' |
| 59 | + elif filled_checkbox: |
| 60 | + #text += '[X] ' |
| 61 | + text += '' |
| 62 | + if ipd: |
| 63 | + text += orglink(ipd) + ' ' |
| 64 | + text += message |
| 65 | + if omit_print: |
| 66 | + return text |
| 67 | + else: |
| 68 | + print_line(text) |
| 69 | + |
| 70 | + |
| 71 | +def thing2string(data, addlink=False): |
| 72 | + "simplify a set (or list) like (['foo', 'bar']) to 'foo, bar'" |
| 73 | + |
| 74 | + if len(data) < 1: |
| 75 | + return '' |
| 76 | + |
| 77 | + result = '' |
| 78 | + for item in data: |
| 79 | + if addlink: |
| 80 | + result += orglink(item) + ' ' |
| 81 | + else: |
| 82 | + result += item + ', ' |
| 83 | + if addlink: |
| 84 | + return result[:-1] |
| 85 | + else: |
| 86 | + return result[:-2] |
| 87 | + |
| 88 | + |
| 89 | +def orglink(text): |
| 90 | + "Takes a text like 'IPD-1234' and generates an Org-mode link" |
| 91 | + |
| 92 | + return '[[' + text.replace('IPD-', 'https://product.infonova.at/jira/browse/IPD-') + '][' + text.replace('IPD-', '') + ']]' |
| 93 | + |
| 94 | + |
| 95 | +def get(issues, ipd, query): |
| 96 | + |
| 97 | + return issues[ipd][query] |
| 98 | + |
| 99 | +def extract_issue_fields(issue): |
| 100 | + |
| 101 | + if issue.fields.customfield_10607: |
| 102 | + team = issue.fields.customfield_10607.value.encode('latin-1', 'ignore') |
| 103 | + else: |
| 104 | + team = None |
| 105 | + |
| 106 | + if issue.fields.assignee: |
| 107 | + assignee = issue.fields.assignee.name.encode('latin-1', 'ignore') |
| 108 | + assigneelong = issue.fields.assignee.displayName.encode('latin-1', 'ignore') |
| 109 | + else: |
| 110 | + assignee = None |
| 111 | + assigneelong = None |
| 112 | + |
| 113 | + if not issue.fields.customfield_11800: |
| 114 | + ghsprint = None |
| 115 | + else: |
| 116 | + ghsprint = re.sub(r'.+name=(.+?),.+$', r'\1', issue.fields.customfield_11800[0]), |
| 117 | + |
| 118 | + return {'key': issue.key[4:], |
| 119 | + 'fix_versions': [x.name for x in issue.fields.fixVersions], |
| 120 | + 'created': issue.fields.created, |
| 121 | + 'description': issue.fields.description, |
| 122 | + 'issuetype': issue.fields.issuetype.name, |
| 123 | + 'labels': issue.fields.labels, |
| 124 | + 'assigneelong': assigneelong, |
| 125 | + 'assignee': assignee, |
| 126 | + 'reporter': issue.fields.reporter.displayName, |
| 127 | + 'resolution': issue.fields.resolution, |
| 128 | + 'resolutiondate': issue.fields.resolutiondate, |
| 129 | + 'status': issue.fields.status.name, |
| 130 | + 'summary': issue.fields.summary.encode('latin-1', 'ignore'), |
| 131 | + 'ghsprint': ghsprint, |
| 132 | + 'team': team, |
| 133 | + 'priority': issue.fields.priority.name, |
| 134 | + 'affects_versions': [x.name for x in issue.fields.versions] |
| 135 | + } |
| 136 | + |
| 137 | + |
| 138 | + |
| 139 | +def retrieve_data_from_jira(ipds): |
| 140 | + |
| 141 | + try: |
| 142 | + sys.path.insert(0, CONFIGDIR) # add cwd to Python path in order to find config file |
| 143 | + import jiraconfig # here, I was not able to use the CONFIGFILENAME variable |
| 144 | + except ImportError: |
| 145 | + print "\nERROR: Could not find \"" + CONFIGFILENAME + \ |
| 146 | + "\".\nPlease generate such a file in the " + \ |
| 147 | + "same directory as this script with following content and configure accordingly:\n" + \ |
| 148 | + CONFIGTEMPLATE |
| 149 | + sys.exit(11) |
| 150 | + |
| 151 | + try: |
| 152 | + from jira import JIRA |
| 153 | + except ImportError: |
| 154 | + print_line("ERROR: Could not find Python module \"JIRA\".\nPlease install it, e.g., with \"sudo pip install jira\".") |
| 155 | + sys.exit(12) |
| 156 | + |
| 157 | + jira = JIRA('https://product.infonova.at/jira/', basic_auth=(jiraconfig.JIRA_USER, jiraconfig.JIRA_PASSWORD)) |
| 158 | + |
| 159 | + query = 'key = "IPD-' + '" or key = "IPD-'.join(ipds) + '" ORDER BY key' |
| 160 | + queryissues = jira.search_issues(query) |
| 161 | + |
| 162 | + issues = [] |
| 163 | + for issue in queryissues: |
| 164 | + |
| 165 | + if issue.fields.issuetype.name == u'Epic': |
| 166 | + ## if it is an Epic, query for its stories instead |
| 167 | + epicquery = 'project = ipd and type = Story and "Epic Link" = ' + issue.key + ' ORDER BY key' |
| 168 | + #print 'DEBUG: found epic, query=[' + epicquery + ']' |
| 169 | + epicissues = jira.search_issues(epicquery) |
| 170 | + issues.extend(retrieve_data_from_jira([x.key.replace('IPD-', '') for x in epicissues])) |
| 171 | + |
| 172 | + elif issue.fields.issuetype.name == u'Story': |
| 173 | + #print 'DEBUG: found story' |
| 174 | + issues.append(extract_issue_fields(issue)) |
| 175 | + |
| 176 | + else: |
| 177 | + ## report Defects and so forth |
| 178 | + print_line(u'ERROR: IPD-' + issue.key + ' is a ' + issue.fields.issuetype.name + \ |
| 179 | + '. Only Stories and Epics are handled here.') |
| 180 | + sys.exit(13) |
| 181 | + |
| 182 | + return issues |
| 183 | + |
| 184 | +def print_issue(issue): |
| 185 | + |
| 186 | + orgdate = time.strftime("%Y-%m-%d", time.localtime()) |
| 187 | + orgtime = time.strftime("%Y-%m-%d %H:%M", time.localtime()) |
| 188 | + DEFAULTSHORT = 'i' + issue['key'] |
| 189 | + short = DEFAULTSHORT |
| 190 | + |
| 191 | + print_line(u'') |
| 192 | + #print_item(None, u'Query time: ' + query_time) |
| 193 | + print_line(u''' |
| 194 | +** TODO [[IPD:''' + issue['''key'''] + ''']] ''' + issue['''summary'''] + \ |
| 195 | + ''' [0/7] :US_''' + short + ''': |
| 196 | +:PROPERTIES: |
| 197 | +:CREATED: [''' + orgtime + '''] |
| 198 | +:ID: ''' + orgdate + '''-Story-''' + short) |
| 199 | + print_line(u''':END: |
| 200 | +
|
| 201 | + | *IPD* | *Confluence* | *Champ* |''') |
| 202 | + |
| 203 | + if issue['''assignee''']: |
| 204 | + champ = issue['''assignee'''] |
| 205 | + else: |
| 206 | + champ = '''-''' |
| 207 | + |
| 208 | + print_line(u''' | [[IPD:''' + issue['''key'''] + '''][''' + issue['''key'''] + ''']] | ''' + \ |
| 209 | + issue['''summary'''] + ''' | ''' + champ + ''' | |
| 210 | +
|
| 211 | +*** STARTED create Jira [[IPD:%s]]''' % issue['''key''']) |
| 212 | + print_line(u''':PROPERTIES: |
| 213 | +:CREATED: [''' + orgtime + '''] |
| 214 | +:ID: ''' + orgdate + '''-''' + short + '''-create-jira-ipd |
| 215 | +:BLOCKER: |
| 216 | +:TRIGGER: ''' + orgdate + '''-''' + short + '''-define-champ(NEXT) ''' + \ |
| 217 | + orgdate + '''-''' + short + '''-estimation(NEXT) |
| 218 | +:END: |
| 219 | +
|
| 220 | +- fill out: |
| 221 | + - [X] set reporter |
| 222 | + - [ ] set level red |
| 223 | + - [ ] fixVersion |
| 224 | +
|
| 225 | +*** NEXT create Confluence page with template |
| 226 | +SCHEDULED: <''' + orgdate + '''> |
| 227 | +:PROPERTIES: |
| 228 | +:CREATED: [''' + orgtime + '''] |
| 229 | +:ID: ''' + orgdate + '''-''' + short + '''-create-confluence-page |
| 230 | +:BLOCKER: |
| 231 | +:TRIGGER: ''' + orgdate + '''-''' + short + '''-write-acceptance-criteria(NEXT) |
| 232 | +:END: |
| 233 | +
|
| 234 | +- fill out: |
| 235 | + - [ ] add Jira-Link [[IPD:''' + issue['''key'''] + ''']] |
| 236 | + - [ ] PO |
| 237 | + - [ ] Title |
| 238 | + - [ ] Business Value |
| 239 | +- [ ] add Confluence-short-URL to story table above |
| 240 | +
|
| 241 | +*** TODO write Acceptance Criteria, Docu, Perms |
| 242 | +:PROPERTIES: |
| 243 | +:CREATED: [''' + orgtime + '''] |
| 244 | +:ID: ''' + orgdate + '''-''' + short + '''-write-acceptance-criteria |
| 245 | +:BLOCKER: ''' + orgdate + '''-''' + short + '''-create-confluence-page |
| 246 | +:TRIGGER: ''' + orgdate + '''-''' + short + '''-confidence-green(NEXT) ''' + \ |
| 247 | + orgdate + '''-''' + short + '''-hand-over-team(NEXT) |
| 248 | +:END: |
| 249 | +
|
| 250 | +*** TODO add Champ to Confluence and Jira :refinement: |
| 251 | +:PROPERTIES: |
| 252 | +:CATEGORY: refinement |
| 253 | +:CREATED: [''' + orgtime + '''] |
| 254 | +:ID: ''' + orgdate + '''-''' + short + '''-define-champ |
| 255 | +:BLOCKER: |
| 256 | +:END: |
| 257 | +
|
| 258 | +*** TODO get Estimation on [[IPD:''' + issue['''key'''] + \ |
| 259 | + ''']] :refinement: |
| 260 | +:PROPERTIES: |
| 261 | +:CREATED: [''' + orgtime + '''] |
| 262 | +:CATEGORY: refinement |
| 263 | +:ID: ''' + orgdate + '''-''' + short + '''-estimation |
| 264 | +:BLOCKER: ''' + orgdate + '''-''' + short + '''-create-jira-ipd |
| 265 | +:TRIGGER: |
| 266 | +:END: |
| 267 | +
|
| 268 | +- Estimation: |
| 269 | +
|
| 270 | +*** TODO get confidence-level green on [[IPD:''' + issue['''key'''] + \ |
| 271 | + ''']] :refinement: |
| 272 | +:PROPERTIES: |
| 273 | +:CATEGORY: refinement |
| 274 | +:CREATED: [''' + orgtime + '''] |
| 275 | +:ID: ''' + orgdate + '''-''' + short + '''-confidence-green |
| 276 | +:BLOCKER: ''' + orgdate + '''-''' + short + '''-write-acceptance-criteria ''' + \ |
| 277 | + orgdate + '''-''' + short + '''-estimation |
| 278 | +:TRIGGER: |
| 279 | +:END: |
| 280 | +
|
| 281 | +*** TODO hand over to team |
| 282 | +:PROPERTIES: |
| 283 | +:CREATED: [''' + orgtime + '''] |
| 284 | +:BLOCKER: ''' + orgdate + '''-''' + short + '''-write-acceptance-criteria ''' + \ |
| 285 | + orgdate + '''-''' + short + '''-estimation |
| 286 | +:ID: ''' + orgdate + '''-''' + short + '''-hand-over-team |
| 287 | +:TRIGGER: ''' + orgdate + '''-''' + short + '''-accept(WAITING) ''' + \ |
| 288 | + orgdate + '''-Story-''' + short + '''(TEAM) |
| 289 | +:END: |
| 290 | +
|
| 291 | +*** acceptance + finish US |
| 292 | +:PROPERTIES: |
| 293 | +:CREATED: [''' + orgtime + '''] |
| 294 | +:ID: ''' + orgdate + '''-''' + short + '''-accept |
| 295 | +:BLOCKER: ''' + orgdate + '''-''' + short + '''-hand-over-team |
| 296 | +:TRIGGER: ''' + orgdate + '''-Story-''' + short + '''(DONE) |
| 297 | +:END: |
| 298 | +''') |
| 299 | + |
| 300 | + |
| 301 | +def main(): |
| 302 | + """Main function""" |
| 303 | + |
| 304 | + ipds = [] |
| 305 | + for argument in args.IPD: |
| 306 | + for ipd in argument.split(' '): |
| 307 | + ## maybe, there is only one argument like "1234 2345" which needs to be splitted: |
| 308 | + ipd_int = int(ipd) |
| 309 | + ipds.append(str(ipd_int)) # make sure that there are only numbers |
| 310 | + |
| 311 | + issues = retrieve_data_from_jira(ipds) |
| 312 | + |
| 313 | + for issue in issues: |
| 314 | + print_issue(issue) |
| 315 | + |
| 316 | + |
| 317 | +if __name__ == "__main__": |
| 318 | + try: |
| 319 | + main() |
| 320 | + except KeyboardInterrupt: |
| 321 | + |
| 322 | + logging.info("Received KeyboardInterrupt") |
| 323 | + |
| 324 | +# END OF FILE ################################################################# |
0 commit comments