"Resolved By" field / get ApplicationUser

Andreas Lorz
Contributor
December 21, 2023

Hello everyone,

We want to have a field that shows the (last) user who solved an issue. We define resolving by the transition when resolution field is set.

I tried to use scriprunner Custom Script Field (User Picker - single user).

I didn't find any best practices for scriprunner and tried to make my own scripted field. However, I failed by returning a com.atlassian.jira.user.ApplicationUser. I can get the userkey, but how do I get an ApplicationUser from a given userkey.

Can anyone help me with this?

 

import com.atlassian.jira.issue.changehistory.ChangeHistoryItem

import com.atlassian.jira.user.ApplicationUser

import com.atlassian.jira.component.ComponentAccessor

def changeHistoryManager = ComponentAccessor.changeHistoryManager

def changer = ApplicationUser

def changerKeyString

// Every resolution change is checked

def changeItems = changeHistoryManager.getAllChangeItems(issue)

changeItems.reverse().each { ChangeHistoryItem item ->

   

   if(item.getField() == "resolution" && item.getTos()){

       log.warn("resolutino changed")

       changer = item.getUserKey()

       changerKeyString = item.getUserKey()

       log.warn(changerKeyString)

       log.warn(item)

   }

}

 

In an test example I get the following:

 

2023-12-21 15:01:51,004 WARN [runner.ScriptBindingsManager]: resolutino changed 
2023-12-21 15:01:51,004 WARN [runner.ScriptBindingsManager]: JIRAUSER152025
2023-12-21 15:01:51,004 WARN [runner.ScriptBindingsManager]: ChangeHistoryItem{id=18724097, changeGroupId=14400601, userKey='JIRAUSER152025', field='resolution', projectId=1234, issueId=123789, issueKey='PLAN-9959', created=2023-10-20 16:14:17.0, nextChangeCreated=292278994-08-17 08:12:55.807, fromValues={}, toValues={10200=Rejected}}

3 answers

1 accepted

0 votes
Answer accepted
PD Sheehan
Rising Star
Rising Star
Rising Stars are recognized for providing high-quality answers to other users. Rising Stars receive a certificate of achievement and are on the path to becoming Community Leaders.
December 22, 2023

The problem with your original code is that the ChangeHistoryItem "userKey" is not actually the user's key or user's name. It's actually the friendly "user display".

You need to fetch the "ChangeHistory" object to know the author of the change.

Here is a short script that should work in your Scripted Field:

import com.atlassian.jira.component.ComponentAccessor

def historyManager = ComponentAccessor.changeHistoryManager

def changeItems = historyManager.getAllChangeItems(issue)
def lastResolutionItem = changeItems.findAll { it.field == 'resolution' && it.tos }.max { it.created }
def user = historyManager.getChangeHistoryById(lastResolutionItem.changeGroupId).authorObject
if (!user) {
log.warn "Could not find user for $issue.key changeHistory with last change of resolution: $lastResolutionItem"
return null
}
return user

As you can see, you can use Groovy collection processing to quickly reduce the list of all change items to A) just those where the resolution field was changed and where the "tos" value is not blank (to exclude elements when the resolution is cleared) and then B) to only show the latest using the "max" method.

Then we get the ChangeHistory record using the changeGroupId and retrieve the authorObject (as ApplicationUser automatically)

 

If you're interested in my suggestions in the other response, you can use a listener for "issueUpdated" event:

 

import com.atlassian.jira.component.ComponentAccessor

def changeHistory = ComponentAccessor.changeHistoryManager.getChangeHistoryById(event.changeLog.id as Long)
def changeItems = changeHistory.changeItemBeans
if(!changeItems.any{it.field == 'resolution' && it.toString}) return //resolution field was not set during this event
event.issue.update{
setCustomFieldValue('Resolved By', changeHistory.authorObject)
}

 

And a console script to back-fill existing issues (adjust the JQL as needed, I used that for my own tests):

import com.atlassian.jira.component.ComponentAccessor

def historyManager = ComponentAccessor.changeHistoryManager
def jql = 'project=ASP and resolved > -80d and resolution is not empty '

Issues.search(jql).each{issue->
def changeItems = historyManager.getAllChangeItems(issue)
def lastResolutionItem = changeItems.findAll{it.field == 'resolution' && it.tos }.max{it.created}
def user = historyManager.getChangeHistoryById(lastResolutionItem.changeGroupId).authorObject
if(!user){
log.warn "Could not find user for $issue.key changeHistory with last change of resolution: $lastResolutionItem"
return null
}
issue.update{
setCustomFieldValue('Resolved By', user)
}
}

You would need to create a new Resolved By field, as this console script and the listener will not work with a scripted field.

0 votes
PD Sheehan
Rising Star
Rising Star
Rising Stars are recognized for providing high-quality answers to other users. Rising Stars receive a certificate of achievement and are on the path to becoming Community Leaders.
December 21, 2023

I would not recommend the scripted field approach in this case.

A user resolving an issue typically happens only once in the lifetime of the issue. But with a scripted field, the script will have to be executed each time you view or index the issue forever and ever.

Scripted fields work best for data that is very dynamic.

In your case, I would recommend either setting the field during a workflow postfunction or using a listener. This way, the code will run once when the is resolved and never again (unless the workflow has a path to go back and re-resolve). This should be more efficient in the long run.

You can run a simple script in your console to back-fill issues that are already resolved (only need to do this once).

Andreas Lorz
Contributor
December 21, 2023

Hi @PD Sheehan ,

appreciate your idea. Seems more logic to me, to do this in postfunction or listener (issue resolved event). 

I guess this is the more wise approche :D BUT...

I will think about that. I guess this will "cost" me more time since there are many workflows in this instance and I have to go over all of them. Checking / changeing postfunctions for setting the field or checking if they throw the correct event. I'm not sure if this was maintained properly in the past.

Tuncay Senturk
Community Leader
Community Leader
Community Leaders are connectors, ambassadors, and mentors. On the online community, they serve as thought leaders, product experts, and moderators.
December 22, 2023

Another downside (apart from modifying each workflow) of this approach is that it will not work for already resolved tickets. 

0 votes
Tuncay Senturk
Community Leader
Community Leader
Community Leaders are connectors, ambassadors, and mentors. On the online community, they serve as thought leaders, product experts, and moderators.
December 21, 2023

Hi @Andreas Lorz 

You have two options:

1. Continue with your current code and use usermanager.getUserByKey(item.getUserKey()) to cast to an ApplicationUser.

2. Alternatively, use `changeHistoryManager.getChangeHistories(issue)` to retrieve all change histories. Then, check if they are related to the resolution field, and if so, use the changeHistory.getAuthorObject() method to return an `ApplicationUser` object."

I hope it helps!

Andreas Lorz
Contributor
December 21, 2023

Hi Tuncay,

thanks for your fast response! I tried both. Howeever, its still not working :( 

I did not manage to cast to ApplicationUser in my first idea. 

Then I started to do your suggested alternative. But this results in Anonymous-User

Any guess?

 

import com.atlassian.jira.issue.changehistory.ChangeHistory

import com.atlassian.jira.issue.history.ChangeItemBean

import com.atlassian.jira.user.util.UserManager

import com.atlassian.jira.issue.changehistory.ChangeHistoryItem

import com.atlassian.jira.user.ApplicationUser

import com.atlassian.jira.component.ComponentAccessor


def changeHistoryManager = ComponentAccessor.changeHistoryManager

def changer = ApplicationUser

def changerKeyString


//Liste <ChangeHistory>

def historys = changeHistoryManager.getChangeHistories(issue)


historys.reverse().each { ChangeHistory history ->

    changer = history.getAuthorObject()

    //log.debug(history.getAuthorObject())


    //Liste <ChangeItemBean>

    def historyEntries = history.getChangeItemBeans()

    //log.error(history)

    historyEntries.reverse().each { ChangeItemBean itembean ->

        if(itembean.getField() == "resolution" && itembean.getTo()){

            log.warn("resolution changed!")

            log.warn(itembean)

            log.warn(history.getAuthorObject())

            changer = history.getAuthorObject()

            return changer

        }

    }    

 }
2023-12-22 08:15:29,384 WARN [runner.ScriptBindingsManager]: resolution changed! 
2023-12-22 08:15:29,384 WARN [runner.ScriptBindingsManager]: com.atlassian.jira.issue.history.ChangeItemBean@73b817ce[fieldType=jira,field=resolution,from=<null>,fromString=<null>,to=1,toString=Fixed,created=2023-12-11 15:15:52.0]
2023-12-22 08:15:29,384 WARN [runner.ScriptBindingsManager]: i00g15(i00g15)
Tuncay Senturk
Community Leader
Community Leader
Community Leaders are connectors, ambassadors, and mentors. On the online community, they serve as thought leaders, product experts, and moderators.
December 22, 2023

Hi @Andreas Lorz 

Did you try returning the result (changer) from the upper block of your code? Instead of returning within each closure of the loop, you could declare a variable outside the loop. Populate this variable with the value as you iterate through the loop, and then return this variable after the loop completes. 

Suggest an answer

Log in or Sign up to answer
DEPLOYMENT TYPE
SERVER
VERSION
8.20.11
TAGS
AUG Leaders

Atlassian Community Events