I wanted to create an interface in Python that had a row of icons at the top. Depending on the screen being displayed, I wanted one of those icons to be highlighted or a different color than the others. 

This proved to be more challenging than I expected.  You can set the color of all of the icons, but setting the color of a single one is a different story.

The solution that I came up with was to create a function that sets the target icon color when the program loads, rather than trying to do it as part of a KivyMD attribute of the TopAppBar widget.

  def set_icon_color(self, dt):
        screen_1_icon = self.screen_1.ids.menu_app_bar.ids.right_actions.children
        #Tell the method where to find the homescreen icon

        screen_1_icon[0].theme_icon_color = "Custom"
        screen_1_icon[0].text_color = "00ADB5"
        #define what the icon should look like

        screen_2_icon = self.screen_2.ids.menu_app_bar.ids.right_actions.children

        screen_2_icon[1].theme_icon_color = "Custom"
        screen_2_icon[1].text_color = "00ADB5"

        screen_2_icon = self.screen_3.ids.menu_app_bar.ids.right_actions.children

        screen_3_icon[2].theme_icon_color = "Custom"
        screen_3_icon[2].text_color = "00ADB5" 

 

We call this function on program load.  This sets the color of the target individual icon on each screen, without affecting the others.   When I switch to any given screen, the appropriate icon is already highlighted with a distinct color.

The other thing I had

Once upon a time, things were simple.  If you wanted to retrieve a page using the Confluence Java API, you simply called getPage().    Fetching Spaces was similarly easy, and intuitive.

Those days are over.  The methods are deprecated. Instead, we now need to use SpaceService and ContentService to manage spaces and content, respectively.  Let’s take a look at some examples of how a task would have been accomplished with the PageManager and SpaceManager, and compare that to how those tasks would be accomplished today.

This the code required to return all page content for all spaces, using PageManager and Spacemanager:
 import com.atlassian.confluence.pages.PageManager
import com.atlassian.sal.api.component.ComponentLocator
import com.atlassian.confluence.spaces.SpaceManager

def spaceManager = ComponentLocator.getComponent(SpaceManager)
def pageManager = ComponentLocator.getComponent(PageManager)

def spaces = spaceManager.getAllSpaces()

spaces.each { space ->
    def pagesInSpace = pageManager.getPages(space, true)
    
    pagesInSpace.each { page ->
        log.warn(page.getBodyAsString())
    }
}
 

 

Here’s the same code, using the SpaceService and ContentService classes:

 import com.atlassian.confluence.api.model.Expansions
import com.atlassian.confluence.api.model.content.ContentRepresentation
import com.atlassian.confluence.api.model.content.ContentBody
import com.atlassian.confluence.api.model.content.Content
import com.atlassian.confluence.api.model.content.Space
import com.atlassian.confluence.api.model.Expansion
import com.atlassian.confluence.api.model.pagination.PageResponse
import com.atlassian.confluence.api.service.content.SpaceService
import com.atlassian.confluence.api.service.content.ContentService
import com.atlassian.confluence.api.model.content.ContentType
import com.onresolve.scriptrunner.runner.ScriptRunnerImpl
import com.atlassian.confluence.api.model.pagination.SimplePageRequest


def contentService = ScriptRunnerImpl.getPluginComponent(ContentService)
def spaceService = ScriptRunnerImpl.getPluginComponent(SpaceService)
 
SimplePageRequest pageRequest = new SimplePageRequest(0, 10)
 
PageResponse < Space > spaceResults = spaceService.find(new Expansion('name')).fetchMany(new SimplePageRequest(0, 10))
 
List < Space > 

 

This script fetches all of the projects in a Jira Cloud instance. It then fetches all of the project roles for that project, and finally fetches all of the users in that role for that project. In this way, it iterates through the projects and returns information about the users in the project roles.

 

 import groovy.json.JsonSlurper

def sb = []
//Define a string buffer to hold the results

def getUsers = get("/rest/api/2/project")
  .header('Content-Type', 'application/json')
  .asJson()
//Get the list of projects in the instance

def content = getUsers.properties.rawBody
//Get the raw body contents of the HTTP response

def scanner = new java.util.Scanner(content).useDelimiter("\\A")
String rawBody = scanner.hasNext() ? scanner.next() : ""
def json = new JsonSlurper().parseText(rawBody)
//Turn the raw body contents into JSON

json.each{ project ->
//Iterate through the projects
  
  sb.add("$project.name")

  def getRoles = get("/rest/api/2/project/$project.id/role")
    .header('Content-Type', 'application/json')
    .asObject(Map)
//For each project, get the list of roles


  getRoles.body.each{ projectRole ->
  //Iterate through the project roles

      def getRoleMembers = get("$projectRole.value")
      .header('Content-Type', 'application/json')
      .asObject(Map)
      //Return the details about each role

    getRoleMembers.body.actors.each{ roleMember ->
    //Get all the actors (users) in that role

        sb.add("$getRoleMembers.body.name:   $roleMember.displayName")
    }
  }
}

return sb
//Return the results


 

Mitigating CORS Errors With Custom Jira REST API Endpoints

If you dive into the world of REST requests and APIs, you may encounter a CORS error that prevents your request from completing. CORS stands for Cross-Origin Resource Sharing.  Same-origin is a security feature in browsers that prevents requests coming from one place (origin) to access resources in a different domain.  CORS allows web pages to access resources on a different network by providing a standard for safely allowing cross-origin requests.

Let’s talk about the example that I encountered.  I wrote a JavaScript macro for Confluence Server, and I was trying to access a third-party API using that macro.  However, Confluence macros run in the browser when the page loads, rather than running on the back-end Confluence server itself.   Thus, while the Confluence server may be set up to address CORS, your browser almost certainly is not, and the request gets blocked.

We can address this by creating a custom REST API endpoint in Confluence (or Jira).   In this way, we have the server making the request to the third party API, and the macro makes the request to the internal API.

In other words, the custom REST API endpoint acts

Overview

Tempo Planner allows for planning team capacity and schedules within Jira.  However, you may have some need to pull that resource planning information out of the Tempo interface and add it to a ticket.

The Tempo API has some severe limitations, but where there’s a will there’s a way.

Team Info

The first thing we’ll examine is how to get information on all of the teams in Tempo Planner.  According the documentation, this isn’t possible.  Per the API documentation, you can return limited very information about plans and allocations. 

Naturally I found this to be unacceptable, and I figured out a way to have the API return all of the teams.   One of the undocumented API endpoints is a search function: /rest/tempo-teams/3/search.    One of the tricks to using this method is that it’s not a GET, it’s a POST, so we have to supply a search parameter as a payload.  When we POST to this endpoint, we supply some JSON: {“teamSearchString”:”<string>”}.  But here’s the rub: the API will accept an empty search string, and return all of the teams as a result.

Allocation Info

Much like team info, there is no public Tempo API endpoint that will

There may come a day when you’re asked to create a large number of Confluence pages. Rather than doing it by hand, why not script it?

This Python script essentially does two things: it reads the CSV file, and it sends page creation requests to a Confluence server.   

For each row in the CSV file, it assumes the page name should be the value in the first cell of the row.  It then generates an HTML table that is sent as part of the page creation request. 

Rather than generating HTML, this could be useful for setting up a large number of template pages, to be filled in by various departments.  It could also run as a job, and automatically create a certain selection of pages every week or month, to store meeting notes or reports.

Please note that in order to connect to the Confluence server, you’ll need to generate a Personal Access Token.

 

 import csv
import requests
import json
import html
import logging

# Initialize logging
logging.basicConfig(level=logging.ERROR)

api_url = 'https://<url>.com/rest/api/content/'
#What's the URL to your Confluence DC instance?


file_path = "<your CSV file path>"
#where is the file stored locally?

parent_page_id = "<your parent page ID>"

The amount of code required to fetch information from Confluence Cloud and bring it into Jira Cloud is a bit shocking. In a good way.

Here’s the code:

 import org.jsoup.*

def authString = "<authstring>"

def fieldConfigsResult = get("https://<url>.atlassian.net/wiki/rest/api/content/229377?expand=body.storage")
  .header('Content-Type', 'application/json')
  .header("Authorization", "Basic ${authString}")
  .asObject(Map)

def storage = fieldConfigsResult.body.body.storage.value


return storage
 

 

In the end it’s all just REST.  So long as you can authenticate, UNIREST allows us to pretty easily fetch information from other sites.

If you’d like to learn more about authenticating against Jira Cloud, check out my post on the subject.

The request on the Atlassian Forums that caught my eye last night was a request to return all Jira Cloud attachments with a certain extension.   Ordinarily it would be easy enough to cite ScriptRunner as the solution to this, but the user included data residency concerns in his initial post.

My solution to this was to write him a Python script to provide simple reporting on issues with certain extension types.    Most anything that the rest API can accomplish can be accomplished with Python; you don’t HAVE to have ScriptRunner.

The hardest part of working with external scripts is figuring out the authorization scheme. Once you’ve got that figured out, the rest is just the same REST API interaction that you’d get with UniREST and ScriptRunner for cloud.

Authorizing the Script

First, read the instructions: https://developer.atlassian.com/cloud/jira/platform/basic-auth-for-rest-apis/ 

Then:

1. Generate an API token: https://id.atlassian.com/manage-profile/security/api-tokens

2. Go to https://www.base64encode.net/ (or figure out the Python module to do the encoding)

3. Base-64 encode a string in exactly this format: youratlassianlogin:APIToken.   
If your email is john@adaptamist.com and the API token you generated is:

ATATT3xFfGF0nH_KSeZZkb_WbwJgi131SCo9N-ztA3SAySIK5w3qo9hdrxhqHZAZvimLHxbMA7ZmeYRMMNR

 

Then the string you base-64 encode is:

john@adaptamist.com:ATATT3xFfGF0nH_KSeZZkb_WbwJgi131SCo9N-ztA3SAySIK5w3qo9hdrxhqHZAZvimLHxbMA7ZmeYRMMNR

 

Do not forget the colon between the two pieces.

This script takes a list of custom field names, and searches each issue in the instance for places where that custom field has been used (i.e., where it has a value other than null).

In this way, we gain insight into the usage of custom fields within a Jira Cloud instance.

The script is interesting for a number of reasons. First, it’s another instance of us having to parse a rawBody response before we can make use of it. Second, it handles the need for pagination, which we’ve also talked about in recent posts.

My intent for this script is for it to serve as a basis for future custom field work in Jira Cloud. Namely, I’d like to be able to easily rename any field with “migrated” in the title.

 

 import groovy.json.JsonSlurper
 
def stillPaginating = true
//Loop to paginate
 
def startAt = 0
//Start at a pagination value of 0
 
def maxResults = 50
//Increment pagination by 50
 
def issueIDs = []
def fieldNames = ["Actual end", "Actual start", "Change risk", "Epic Status"]
def customFieldMap = [: ]
 
 
//Get all the fields in the system
def fieldIDs = get("/rest/api/3/field")
  .header('Content-Type', 'application/json')
  .header('Accept', 'application/json')
  .asBinary()
 
def inputStream = fieldIDs.rawBody

Get All Filters in a Jira System

Here’s the truth: getting all of the filters in a Jira DC instance with ScriptRunner is awkward and fussy.  There’s no method that simply returns all of the filters. 

Instead, we need to first return all of the users in the system.  And then we need to examine all of the filters that each of them owns, as each filter must have an owner.  Here’s an example of some code that does that:

 import com.atlassian.jira.user.ApplicationUser
import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.bc.filter.SearchRequestService
import com.atlassian.jira.issue.search.SearchRequest
import com.atlassian.jira.bc.user.search.UserSearchParams
import com.atlassian.jira.bc.user.search.UserSearchService
   
   
SearchRequestService searchRequestService = ComponentAccessor.getComponent(SearchRequestService.class)
UserSearchService userSearchService = ComponentAccessor.getComponent(UserSearchService)
def sb = new StringBuffer()
   
UserSearchParams userSearchParams = new UserSearchParams.Builder()
    .allowEmptyQuery(true)
    .includeInactive(false)
    .ignorePermissionCheck(true)
    .build()
//Define the parameters of the query
  
//Iterate over each user's filters
userSearchService.findUsers("", userSearchParams).each{ApplicationUser filter_owner ->
    try {
        searchRequestService.getOwnedFilters(filter_owner).each{SearchRequest filter->
            String jql = filter.getQuery().toString()
            //for each fiilter, get JQL and check if it contains our string
             
                sb.append("Found: ${filter.name}, ${jql}\n" + "<br>")
             
        }
    } catch (Exception e) {
            //if filter is private
           sb.append("Unable to get filters for ${filter_owner.displayName} due to ${e}")
    }
}
 
return sb 

 

Getting a list of filters on Jira Cloud is much simpler, as there’s a REST API that accomplishes this.  If we