Add Tempo Planning Information

automatefields

Overview

Gather all existing Tempo plans for a Jira issue (defined by its ID from Jira, and interpreted as the planItemId in Tempo) for a given period (start date to end date) and get total time planned, using the Tempo REST API. This script uses the Tempo REST Endpoint with basic authentication in the headers of the request. The authenticated user must have permission to view the plans for users; otherwise, the API does not return plans. Read more about permissions in Tempo in this article.

Example

As a project manager, I want to see the total time planned for projects in my next sprint. I need to know if the planned work is greater than my team capacity. I can use this script to see the total planned time for selected projects over the two week sprint period.

Good to Know

  • This script requires [Tempo Planner by Tempo for Jira]Tempo Planner.
  • API request credentials are stored as user properties. This property is named as basicAuthCreds and it contains user and password in the following format: <user>:<password>.
  • For an example of setting/getting user properties, see Library documentation.
  • Set the script listener for the AllocationEvent Tempo event, so once a Tempo item is created, updated or deleted, the script is executed.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 import com.atlassian.jira.component.ComponentAccessor import com.atlassian.jira.config.properties.APKeys import com.atlassian.jira.event.type.EventDispatchOption import com.onresolve.scriptrunner.runner.customisers.WithPlugin import com.tempoplugin.planner.api.event.AllocationEvent import groovyx.net.http.ContentType import groovyx.net.http.HttpResponseDecorator import groovyx.net.http.RESTClient import groovyx.net.http.EncoderRegistry import java.time.Duration import java.time.LocalDate import java.time.format.DateTimeFormatter import java.util.concurrent.TimeUnit @WithPlugin('com.tempoplugin.tempo-plan-core') // The user-defined property where the user name and password are stored into final userPropertyKey = 'jira.meta.basicAuthCreds' final plannedTimeField = 'Planned Time' /** * Helper method to print Duration time as hours, minutes and seconds. * @param duration Time to be formatted. * @return Duration formatted as String. */ String prettyPrintDuration(Duration duration) { String.format("%s h ${duration.toMinutes() == 0 ? '%s m' : ''} ${duration.seconds == 0 ? '%s s' : ''}", duration.toHours(), duration.toMinutes() - TimeUnit.HOURS.toMinutes(duration.toHours()), duration.seconds - TimeUnit.MINUTES.toSeconds(duration.toMinutes())).trim() } def loggedInUser = ComponentAccessor.jiraAuthenticationContext.loggedInUser def credentials = ComponentAccessor.userPropertyManager.getPropertySet(loggedInUser).getString(userPropertyKey) def baseUrl = ComponentAccessor.applicationProperties.getString(APKeys.JIRA_BASEURL) def client = new RESTClient(baseUrl) client.encoderRegistry = new EncoderRegistry( charset: 'UTF-8' ) client.setHeaders([ Authorization : "Basic ${credentials.bytes.encodeBase64()}", "X-Atlassian-Token": "no-check" ]) client.handler.failure = { HttpResponseDecorator response -> log.error response.entity.content.text [] } client.handler.success = { resp, reader -> [response: resp, reader: reader] } def today = LocalDate.now() def todayPlus90Days = today.plusDays(90) def event = event as AllocationEvent def allocationResult = (client.get( path: '/rest/tempo-planning/1/allocation', query: [ planItemId : event.allocation.planItemId, planItemType: 'ISSUE', assigneeType: 'user', startDate : DateTimeFormatter.ISO_LOCAL_DATE.format(today), endDate : DateTimeFormatter.ISO_LOCAL_DATE.format(today) ] ) as Map).reader as List<Map> if (!allocationResult) { log.error "There is no allocation result related with the plan" return } def taskKey = (allocationResult.first()?.planItem as Map)?.key if (!taskKey) { log.error "There is no issue related with the plan" return } def planSearchResult = (client.post( path: '/rest/tempo-planning/1/plan/search', contentType: ContentType.JSON, body: [ from : DateTimeFormatter.ISO_LOCAL_DATE.format(today), to : DateTimeFormatter.ISO_LOCAL_DATE.format(todayPlus90Days), taskKey: [taskKey] ] ) as Map).reader as List<Map> if (!planSearchResult) { log.error "There is no time planned related with the plan" return } def totalSeconds = planSearchResult.sum { (it as Map).timePlannedSeconds } def duration = Duration.ofSeconds(totalSeconds as Long) def plannedTime = prettyPrintDuration(duration) def cfManager = ComponentAccessor.customFieldManager def plannedTimeCf = cfManager.getCustomFieldObjectsByName(plannedTimeField).first() def issueManager = ComponentAccessor.issueManager def issue = issueManager.getIssueObject(taskKey as String) issue.setCustomFieldValue(plannedTimeCf, plannedTime) issueManager.updateIssue(loggedInUser, issue, EventDispatchOption.DO_NOT_DISPATCH, false)