Skip to content

Commit

Permalink
Merge pull request #18 from lhh/devel
Browse files Browse the repository at this point in the history
Support setting resolution on 'done' transitions
  • Loading branch information
lhh authored Jan 24, 2024
2 parents ef49da7 + 13dd391 commit 360aa37
Show file tree
Hide file tree
Showing 6 changed files with 101 additions and 23 deletions.
39 changes: 24 additions & 15 deletions jirate/jboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -325,7 +325,7 @@ def comment(self, issues, text, visibility=None):
ret.append(issue)
return ret

def close(self, issues):
def close(self, issues, **args):
"""Close an issue
XXX this might not be a usable API and/or may be a higher
Expand All @@ -336,11 +336,13 @@ def close(self, issues):
Parameters:
issues: list of issue keys or IDs (list of string)
args: dictionary of fields to set on transition
(resolution only really known right now)
Returns:
requests.Response
"""
return self.move(issues, 'Closed')
return self.move(issues, 'Closed', **args)

def create(self, field_definitions=None, **args):
"""Create a new issue using key/value pairs
Expand Down Expand Up @@ -472,27 +474,25 @@ def transitions(self, issue):
issue_alias: key or issue ID (string)
Returns:
dict - {'state': 'id', 'state2': 'id2'}
list of transitions w/ metadata
"""
if isinstance(issue, str):
issue = self.issue(issue)
possible = {}
url = os.path.join(issue.raw['self'], 'transitions')
url = os.path.join(issue.raw['self'], 'transitions?expand=transitions.fields')
transitions = json_loads(self.jira._session.get(url))
for transition in transitions['transitions']:
possible[transition['to']['id']] = {'id': transition['id'], 'name': transition['to']['name']}
if not possible:
return None
return possible
if transitions:
return transitions['transitions']

def _find_transition(self, issue, status):
transitions = self.transitions(issue)
for state_id in transitions:
if transitions[state_id]['name'] == status or nym(transitions[state_id]['name']) == status or str(state_id) == str(status):
return transitions[state_id]['id']
for transition in transitions:
trans_state = transition['to']['name']
trans_state_id = transition['to']['id']
if trans_state == status or nym(trans_state) == status or str(status) == str(trans_state_id):
return transition
return None

def move(self, issue_list, status):
def move(self, issue_list, status, **args):
"""Execute a transition to move a set of issues to the desired status
Jira doesn't have a status you can update; you have to retrieve possible
Expand All @@ -502,6 +502,7 @@ def move(self, issue_list, status):
Parameters:
issue_aliases: list keys or issue IDs (list of string)
status: Desired status
**args: field=value pairs to set on transition
Returns:
list of successfully moved issues (list of string)
Expand All @@ -518,9 +519,17 @@ def move(self, issue_list, status):
if not transition:
continue

data = {'transition': {'id': transition['id']}}
if args and 'fields' in transition:
new_args = transmogrify_input(transition['fields'], **args)
data['fields'] = new_args
if new_args == {}:
oops = [args.keys()]
raise ValueError(f'field(s) not allowed in transition: {oops}')

# POST /rest/api/2/issue/{issueIdOrKey}/transitions
url = os.path.join(issue.raw['self'], 'transitions')
self.jira._session.post(url, data={'transition': {'id': transition}})
self.jira._session.post(url, data=data)
return issues

def link_types(self):
Expand Down
8 changes: 6 additions & 2 deletions jirate/jira_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,11 @@ def move(args):

def close_issues(args):
ret = 0
close_args = {}
if args.resolution:
close_args['resolution'] = args.resolution
for issue in args.target:
if not args.project.close(issue):
if not args.project.close(issue, **close_args):
ret = 1
return (ret, False)

Expand Down Expand Up @@ -766,7 +769,7 @@ def print_issue(project, issue_obj, verbose=False, no_comments=False, no_format=
vsep_print(None, 'URL', lsize, issue_obj.permalink())
trans = project.transitions(issue_obj.raw['key'])
if trans:
vsep_print(' ', 'Next States', lsize, [tr['name'] for tr in trans.values()])
vsep_print(' ', 'Next States', lsize, [tr['name'] for tr in trans])
else:
vsep_print(None, 'Next States', lsize, 'No valid transitions; cannot alter status')

Expand Down Expand Up @@ -1068,6 +1071,7 @@ def create_parser():
cmd.add_argument('issue', help='Existing Issue (more fields available here)', nargs='?')

cmd = parser.command('close', help='Move issue(s) to closed/done/resolved', handler=close_issues)
cmd.add_argument('-r', '--resolution', help='Set resolution on transition')
cmd.add_argument('target', nargs='+', help='Target issue(s)')

cmd = parser.command('call-api', help='Call an API directly and print the resulting JSON', handler=call_api)
Expand Down
3 changes: 2 additions & 1 deletion jirate/jira_input.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ def in_owc(value):
'securitylevel': in_name,
'option-with-child': in_owc,
'issuelink': in_key,
'resolution': in_name,
'user': in_string # When setting array, you specify 'name': name
# When setting assignee, you just give the name
# as a string
Expand Down Expand Up @@ -103,7 +104,7 @@ def allowed_value_validate(field_name, values, allowed_values=None):
for key in ['name', 'value']:
if key not in av:
continue
if val not in (av[key], nym(av[key])):
if val not in (av[key], nym(av[key]), av[key].lower()):
continue
ret.append(av[key])
found = True
Expand Down
31 changes: 28 additions & 3 deletions jirate/tests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,23 @@
fake_metadata = {val['id']: val for val in fake_fields}


fake_transitions = {'expand': 'transitions',
'transitions': [{'id': '11', 'name': 'New', 'to': {'name': 'New', 'id': '10000'}},
{'id': '12', 'name': 'In Progress', 'to': {'name': 'In Progres', 'id': '10001'}},
{'id': '13', 'name': 'Done', 'to': {'name': 'Done', 'id': '10002'},
'fields':
{'resolution': {
'name': 'Resolution',
'fieldId': 'resolution',
'schema': {
'type': 'resolution',
'system': 'resolution'},
'operations': ['set'],
'allowedValues': [{'name': 'Done'}, {'name': 'Won\'t Do'}, {'name': 'Duplicate'}]}}},
{'id': '14', 'name': 'New', 'to': {'name': 'New', 'id': '10003'}}]
}


fake_issues = {
'TEST-1': {'expand': 'renderedFields,names,schema,operations,editmeta,changelog,versionedRepresentations',
'fields': {'aggregateprogress': {'progress': 0, 'total': 0},
Expand Down Expand Up @@ -468,18 +485,26 @@


class fake_jira_session():
def __init__(self):
self.get_urls = []
self.post_urls = {}
self.delete_urls = []

def get(self, url):
pass
self.get_urls.append(url)

def post(self, url, data=None):
pass
self.post_urls[url] = data

def delete(self, url):
pass
self.delete_urls.append(url)

def close(self):
pass

def reset(self):
self.__init__()


class fake_jira(JIRA):
def __init__(self, **kwargs):
Expand Down
6 changes: 5 additions & 1 deletion jirate/tests/test_input.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#!/usr/bin/env python

from jirate.tests import fake_metadata
from jirate.tests import fake_metadata, fake_transitions
from jirate.jira_input import transmogrify_input

import os
Expand Down Expand Up @@ -141,3 +141,7 @@ def test_trans_version_value():
def test_trans_missing_version():
with pytest.raises(ValueError):
transmogrify_input(fake_metadata, **{'version_value': '999'})


def test_trans_metadata():
transition_fields = fake_transitions
37 changes: 36 additions & 1 deletion jirate/tests/test_jirate.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,23 @@
#!/usr/bin/env python

from jirate.jboard import Jirate
from jirate.tests import fake_jira, fake_user
from jirate.tests import fake_jira, fake_user, fake_transitions

import pytest # NOQA
import types


fake_jirate = Jirate(fake_jira())


def transitions_override(obj, issue):
return fake_transitions['transitions']


# XXX we don't have subs in at the level we need, so fake transitions this way
fake_jirate.transitions = types.MethodType(transitions_override, fake_jirate)


def test_jirate_myself():
assert not fake_jirate._user
me = fake_jirate.user
Expand Down Expand Up @@ -72,3 +81,29 @@ def test_jirate_customfields():
# Negative test
with pytest.raises(AttributeError):
issue.field('arglebargle')


def test_transition_resolutions():
issue = fake_jirate.issue('TEST-1')
assert fake_jirate.transitions(issue) == fake_transitions['transitions']

fake_jirate.jira._session.reset()
assert fake_jirate.move('TEST-1', 'done') == [issue]
assert fake_jirate.jira._session.post_urls == {'https://domain.com/rest/api/2/issue/1000001/transitions': {'transition': {'id': '13'}}}

fake_jirate.jira._session.reset()
assert fake_jirate.move('TEST-1', 'done', resolution='won\'t do') == [issue]
assert fake_jirate.jira._session.post_urls == {'https://domain.com/rest/api/2/issue/1000001/transitions': {'fields': {'resolution': {'name': 'Won\'t Do'}}, 'transition': {'id': '13'}}}

fake_jirate.jira._session.reset()
assert fake_jirate.move('TEST-1', 'done', resolution='done') == [issue]
assert fake_jirate.jira._session.post_urls == {'https://domain.com/rest/api/2/issue/1000001/transitions': {'fields': {'resolution': {'name': 'Done'}}, 'transition': {'id': '13'}}}


def test_transition_bad_field():
issue = fake_jirate.issue('TEST-1')
assert fake_jirate.transitions(issue) == fake_transitions['transitions']

fake_jirate.jira._session.reset()
with pytest.raises(ValueError):
assert fake_jirate.move('TEST-1', 'done', beastly_fido='odif_yltsaeb') == [issue]

0 comments on commit 360aa37

Please sign in to comment.