Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support setting resolution on 'done' transitions #18

Merged
merged 4 commits into from
Jan 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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]