In this page

Filtering, sorting the issues passed to the PDF template

Filtering issues

In most of the cases, you don't need a secondary filter. JQL, the query language used by Jira, is extremely flexible and allows implementing complex searches. You should just set up a saved filter in Jira, run that and export the result set.

In those cases when JQL is not sufficient, or you really need to execute a second filter, you can do that in the template.

How? Each template contains a main loop that iterates over the issues like this:

#foreach($issue in $issues)
	## ... export body code omitted
#end

You should rewrite that in the following way to evaluate an extra condition and to export only if that condition evaluates to true:

#foreach($issue in $issues)
	## only export the issues in the 'DEV' project and ignore others
	#if($issue.key.contains("DEV"))
		## ... export body code omitted
	#end
#end

Sorting issues to export those in a different order

In most of the cases, you can flexibly sort issues in JQL using ORDER BY.

If you need to sort according to some more complex logic, follow this pattern:

  1. Create a sorter class in Groovy that implements your ordering and save it as sorter-tool.groovy:
    sorter = new SorterTool()
    
    public class SorterTool {
    	public sort(issues) {
    		return issues.sort { a, b -> a.summary <=> b.summary }
    	}
    }
    
  2. Execute it in your template:
    $scripting.execute('sorter-tool.groovy')
  3. Pass the incoming collection $issues (a Velocity context parameter) to the Groovy code and iterate over the re-sorted collection like this:
    #set($sortedIssues = $sorter.sort($issues))
    #foreach($issue in $sortedIssues)
    	## ... export body code omitted
    #end
    

You can implement any kind of flexible sorting logic based on this example.

Sorting the comments as "newest first"

Comments for an issue are shown in ascending order by default, i.e. the most recent one is displayed at the bottom. To reverse this order:

  1. Write a short sorter class in Groovy that implements your ordering and save it as comment-sorter.groovy:
    commentSorter = new CommentSorterTool()
    
    class CommentSorterTool {
    	def descending(comments) {
    		return comments.sort { a, b -> b.created <=> a.created }
    	}
    }
    
  2. Execute it in your template:
    $scripting.execute('comment-sorter.groovy')
  3. Change the line
    #set($comments = $pdfContent.commentsByIssue($issue))
    to:
    #set($comments = $commentSorter.descending($pdfContent.commentsByIssue($issue)))

Formatting data

Formatting numbers

There is a tool called $number you can use in the template code:

$number.format('###,###.00', $unitPrice)

The first argument is the format string, the second argument is the numerical value.

Formatting dates

There are multiple tools to format date, time and date-time values, each with its own merits:

  1. $userDateTimeFormatter is the new standard way to format dates in Jira. This should be your default choice.
  2. $date should be used when you need to format dates with full control. It supports using the standard Java date format patterns, as seen in the code below.

These code snippets show formatting $currentDate with all the methods:

<fo:block>
	Using DateTimeFormatter (user timezone, date and time):
	$userDateTimeFormatter.withStyle($dateTimeStyle.COMPLETE).format($currentDate)
	## output: 16/Sep/14 9:15 AM
</fo:block>
<fo:block>
	Using DateTimeFormatter (user timezone, date only):
	$userDateTimeFormatter.withStyle($dateTimeStyle.DATE).format($currentDate)
	## output: 16/Sep/14
</fo:block>
<fo:block>
	Using DateTimeFormatter (default timezone, date only):
	$userDateTimeFormatter.withDefaultZone().withStyle($dateTimeStyle.DATE).format($currentDate)
	## output: 16/Sep/14
</fo:block>
<fo:block>
	Using DateTimeFormatter (user timezone, RFC822 format):
	$userDateTimeFormatter.withStyle($dateTimeStyle.RSS_RFC822_DATE_TIME).format($currentDate)
	## output: Tue, 16 Sep 2014 09:15:26 +0200
</fo:block>
<fo:block>
	Using DateTool:
	$date.format("yyyy-'W'ww-EEE", $currentDate)
	## output: 2014-W38-Tue
</fo:block>

Although the most typical examples are the issue key and summary fields, you can generate hyperlinks for any piece of information using the example below.

This allows users intuitively click issue keys or summaries to jump to the Jira page of the corresponding issue.

<fo:basic-link color="#036" text-decoration="underline" external-destination="url('${requestContext.baseUrl}/browse/$xmlutils.escape($issue.key)')">$xmlutils.escape($issue.key)</fo:basic-link>

Math

Basic math operations

A Velocity tool called $math is available for the templates. You can use it for basic mathematical operations, like adding the value of two number type custom fields.

Example:

#set($qty = $issue.getCustomFieldValue("customfield_10000"))
#set($unitPrice = $issue.getCustomFieldValue("customfield_10001"))

#set($linePrice = $math.mul($qty, $unitPrice))
#set($totalPrice = $math.add($totalPrice, $linePrice))

Complex math

If you need more than this basic math, you will write Groovy scripts. See the scripting tutorial first, then continue learning about Groovy operators.

Sub-tasks

An issue can return its sub-tasks using the following getter:

Collection<Issue> getSubTaskObjects()

Exporting sub-tasks

If you want to iterate over sub-tasks and export those, the iteration looks like this:

## assuming that you have the parent issue available in $issue
#foreach($subTask in $issue.subTaskObjects)
	## ... export the $subTask object here the same way as a top-level issue
#end

Exporting sub-tasks exactly the same way as top-level issues

If you want to export sub-tasks in the same way (same fields, same formatting, etc.) as the top-level isssues, you can use a simple trick: prepare a list by merging the top-level issues and their sub-tasks, and then iterate over the merged list!

If you are working with issue-fo.vm template, you only have to set this configuration variable in the top to true:

#set($exportSubTasks = true)

For other templates, follow these steps:

  1. Find this iteration in the template:
    #if($issues && !$issues.empty)
    	#foreach($issue in $issues)
    	## ...
    
  2. Modify that to this:
    #if($issues && !$issues.empty)
    	## merge top-level issues and sub-tasks
    	#set($allIssues = [])
    	#foreach($issue in $issues)
    		## assign the return value to a dummy variable to hide that in the output
    		#set($dummmy = $allIssues.add($issue))
    		#foreach($subTask in $issue.subTaskObjects)
    			#set($dummmy = $allIssues.add($subTask))
    		#end
    	#end
    	## iterate over the merged list
    	#foreach($issue in $allIssues)
    	## ...
    

Important: make sure to filter out sub-tasks by using a JQL like this, otherwise those will appear twice in the export:

project = FOOBAR AND type != Sub-task

(You can, alternatively, filter out sub-tasks while creating the merged list, but doing that with JQL is simpler.)

Exporting sub-task custom fields

  1. Modify the declaration of the #cfValue() macro in issue-fo.vm to also accept sub-task objects. Just replace this line:
    ## renders custom field values
    #macro(cfValue $customField)
    
    ...with:
    ## renders custom field values
    #macro(cfValue $customField)
    		#cfValueForIssue($customField $issue)
    	#end
    
    ## renders custom field values for the passed issue
    #macro(cfValueForIssue $customField $issue)
    
  2. You can then export custom field values from sub-tasks like:
    #foreach($subTask in $issue.subTaskObjects)
    	## ...
    	<fo:block>#cfValueForIssue($subTask.getCustomField("customfield_10007") $subTask)</fo:block>
    	## ...
    #end
    

Exporting parent issues of sub-tasks

Typically you export sub-tasks while exporting their parents, but sometimes the situation may be reversed. In that case, any field of the parent issue is accessible from a sub-task through its parentObject property.

You can, for example, get the key of the parent issue with this expression:

${subTask.parentObject.key}

Searching for issues

In addition to the issues passed to the template, it is possible to execute further JQL searches and also use those issues in your template.

With JQL queries

Execute a JQL query and iterate through the results:

#set($issuesFound = $jqlSearch.searchByJql("project=FOOBAR ORDER BY summary"))
#foreach($issue in $issuesFound)
	<fo:block>[$xmlutils.escape($issue.key)] $xmlutils.escape($issue.summary)</fo:block>
#end

With saved filters

Execute the saved filter with the ID=13100 and iterate through the results:

#set($issuesFound = $jqlSearch.searchBySavedFilter(13100))
#foreach($issue in $issuesFound)
	<fo:block>[$xmlutils.escape($issue.key)] $xmlutils.escape($issue.summary)</fo:block>
#end

Connecting to REST APIs

If you need to connect to REST services, that's pretty easy, too.

Connecting to the Jira REST API

This example demonstrates connecting to the Jira REST API using BASIC authentication and getting an issue.

  1. Create the script called jira-rest-api-tool.groovy that implements the REST API invocation:
    import groovy.json.JsonSlurper
    import org.apache.commons.io.IOUtils
    
    def user = "admin"
    def password = "admin"
    def urlConnection = new URL("https://myenvironment.atlassian.net/rest/api/3/issue/DEMO-1").openConnection()
    urlConnection.setRequestProperty("Authorization", "Basic " + (user + ":" + password).bytes.encodeBase64().toString())
    def jsonString = IOUtils.toString(urlConnection.inputStream)
    
    issueRest = new JsonSlurper().parseText(jsonString)
    
  2. Execute it in your template:
    $scripting.execute('jira-rest-api-tool.groovy')
  3. Use the issueRest object to access the returned issue's fields in the template:
    ${issueRest.key}
    ${issueRest.fields.summary}
    ${issueRest.fields.status.name}
    

Connecting to external REST APIs

This is an example of calling an external REST API without authorization:

  1. Create the script called external-rest-api-tool.groovy that implements the REST API invocation:
    import groovy.json.JsonSlurper
    
    def jsonSlurper = new JsonSlurper()
    dataRest = jsonSlurper.parseText(new URL("http://services.groupkt.com/country/get/iso2code/US").text)
    
  2. Execute it in your template:
    $scripting.execute('external-rest-api-tool.groovy')
  3. Use the dataRest object to access the returned information in the template:
    ${dataRest.RestResponse.result.name}
    ${dataRest.RestResponse.result.alpha2_code}
    

More on REST authentication

Some thoughts on REST authentication:

  • If you need to pull data from the running Jira instance only, prefer using our helpers and tools over the REST API. That's faster, easier and you completely eliminate the need for authentication.
  • If you are worried about using BASIC authentication, it is basically fine if used over HTTPS. If that's the case in your environment, keep it simple and just use BASIC.
  • You have full control over the user account used for making REST calls. This means, you can set up a dedicated, restricted Jira user for REST. For instance, create a user account named rest-client-acc, remove all "write" permissions, only add "read" permissions for certain projects, and then use this account in REST calls.

Graphics

Exporting project avatar images

To export project avatars, just construct their URLs:

## assuming that the issue is available in $issue
<fo:block>
	#set($project = $issue.projectObject)
	#set($avatarImageUrl = "${requestContext.baseUrl}/secure/projectavatar?pid=${project.id}&avatarId=${project.avatar.id}")
	<fo:external-graphic content-height="4em" vertical-align="middle" src="url($xmlutils.escape($avatarImageUrl))"/>
	$xmlutils.escape($project.name)
</fo:block>

Exporting user avatar images

To export user avatars, just construct their URLs:

## assuming that the issue is available in $issue
<fo:block>
	#if($issue.assignee)
		#set($avatarImageUrl = "${requestContext.baseUrl}/secure/useravatar?avatarId=${avatarService.getAvatar($user, $issue.assignee.name).id}&ownerId=${issue.assignee.name}")
		<fo:external-graphic content-height="2em" vertical-align="middle" src="url($xmlutils.escape($avatarImageUrl))"/>
		$xmlutils.escape($issue.assignee.displayName)
	#else
		$i18n.getText("common.concepts.unassigned")
	#end
</fo:block>

Exporting issue type icon images

To export issue type icons, just use the URLs returned by the issues:

## assuming that the issue is available in $issue
<fo:block>
	#if(${issue.issueTypeObject.completeIconUrl})
		#set($iconImageUrl = ${issue.issueTypeObject.completeIconUrl})
	#else
		#set($iconImageUrl = ${issue.issueTypeObject.iconurl})
	#end
	<fo:external-graphic content-height="1.5em" vertical-align="middle" src="url($xmlutils.escape($iconImageUrl))"/>
	$xmlutils.escape($issue.issueTypeObject.nameTranslation)
</fo:block>

Exporting priority icon images

To export priority icons, just use the URLs returned by the issues:

## assuming that the issue is available in $issue
<fo:block>
	#if(${issue.priorityObject})
		#if(${issue.priorityObject.completeIconUrl})
			#set($iconImageUrl = ${issue.priorityObject.completeIconUrl})
		#else
			#set($iconImageUrl = ${issue.priorityObject.iconurl})
		#end
		<fo:external-graphic content-height="1.5em" vertical-align="middle" src="url($xmlutils.escape($iconImageUrl))"/>
		$xmlutils.escape($issue.priorityObject.nameTranslation)
	#end
</fo:block>

Dynamic graphics

You can vastly improve the readability and value of your PDF documents with graphical illustrations. For static graphics, simply use images. For dynamic graphics, there are two technology options detailed in the next sections.

SVG graphics

Using embedded SVG is a powerful and simple way to add dynamic vector graphics to your templates. This can be utilized to solve a plethora of problems, from rotating text, drawing lines, to enhancing your documents with geometric shapes. See the next sections for some typical use cases, and study this SVG tutorial for more examples.

Rotating text

When the horizontal space is limited, you may want display texts like the issue summary rotated to vertical position:

## assuming that the issue is available in $issue
<fo:block>
	<fo:instream-foreign-object>
		<svg:svg xmlns:svg="http://www.w3.org/2000/svg" width="11" height="300">
			<svg:text transform="rotate(-90)" style="text-anchor:end;" x="0" y="9" font-size="9">$xmlutils.escape($issue.summary)</svg:text>
		</svg:svg>
	</fo:instream-foreign-object>
</fo:block>
Drawing lines

You can easily draw arbitrary lines or arrows, to enhance your documents. This example use lines to add red strike-through decoration over text:

## assuming that the issue is available in $issue
<fo:block>
	<fo:instream-foreign-object>
		<svg:svg xmlns:svg="http://www.w3.org/2000/svg" width="500" height="11">
			<svg:text style="text-anchor:start;" x="0" y="9" font-size="9">$xmlutils.escape($issue.summary)</svg:text>
			<svg:line x1="0" y1="5%" x2="100%" y2="95%" style="stroke:rgb(255,0,0);"/>
			<svg:line x1="0" y1="95%" x2="100%" y2="5%" style="stroke:rgb(255,0,0);"/>
		</svg:svg>
	</fo:instream-foreign-object>
</fo:block>

Groovy graphics

When you have graphic that is drawn by complicated logic that would be difficult to implement in SVG, you can always use Groovy scripting.

The idea:

  1. Create the Groovy class that implements the drawing logic.
  2. The class should provide high-level methods that accept the arguments that affect the resulted graphic and return data URI strings.
  3. First, the methods create the images in memory using whatever graphic technology you prefer, and serialize the images to PNG (byte arrays in memory). Then they convert the byte arrays to data URIs, and return those as strings.
  4. In the template, execute the script, and use snippets like the one below to display the graphic:
    <fo:block>
    	<fo:external-graphic content-height="4cm" src="data:image/png;base64,$gfx.drawFooImage($issue, 400, 300)"/>
    </fo:block>
    

(This is essentially the same technique, how the the JFreeChart rendered custom charts are inserted to templates, which is, in fact, a similar problem in nature.)

Auto-selecting templates

Auto-selecting templates by project

Say, you have two templates issue-fo-foo.vm and issue-fo-bar.vm. You want to define a single PDF view "My PDF export" which should render issue-fo-foo.vm in the "TEST" project and issue-fo-bar.vm in all other projects. In other words, the PDF view should intelligently select the template based on the project of the first passed issue.

Steps:

  1. Use the following dispatcher code in the main issue-fo.vm. This reads the project key from the first passed issue and dispatches to different templates based on the project key:
    <?xml version="1.0" encoding="ISO-8859-1"?>
    
    ## dispatch to a template based on the project key
    #set($projectKey = $issues.get(0).project.key)
    #if($projectKey == 'TEST')
    	$include.parse($ctx, "issue-fo-foo.vm")
    #else
    	$include.parse($ctx, "issue-fo-bar.vm")
    #end
  2. Create the two actual templates issue-fo-foo.vm and issue-fo-bar.vm through the Template Manager.
  3. Remove the first line in each of the actual templates. Delete this in both issue-fo-foo.vm and issue-fo-bar.vm:
    <?xml version="1.0" encoding="ISO-8859-1"?>
    If you forget this, then this declaration will be there both in the first line of the dispatcher template (which is fine) and in the first lines of the actual templates (which is not allowed and will raise an XML parsing error).

Using this technique, you can select different templates based on the view mode (single issue or search request), on the number of issues (single or multiple), on field values, or on any other condition that can be evaluated in the rendering context.

Auto-selecting templates by issue type

As another example, here is the code that selects the template based on type of the issues:

<?xml version="1.0" encoding="ISO-8859-1"?>

## dispatch to a template based on the issue type identifier
#set($issueTypeId = $issues.get(0).issueTypeObject.id)
#if($issueTypeId == '6')
	$include.parse($ctx, "issue-fo-foo.vm")
#else
	$include.parse($ctx, "issue-fo-bar.vm")
#end

Other tips & tricks

Embedding issue attachments in the exported PDFs

See the related tutorial.

Alternating row colors (zebra stripes)

If you want to add alternating row colors to your templates, just calculate the style based on the loop counter:

#foreach($issue in $issues)
	## select the style depending on whether the loop counter is odd or even
	#set($coloredRow = "")
	#if($velocityCount % 2 == 0)
		#set($coloredRow = 'background-color="#ffffb3"')
	#end
	<fo:block $coloredRow>$xmlutils.escape($issue.summary)</fo:block>
#end

It is up to you to implement more complex coloring based on this example.

Customizing column widths in the "issue-navigator-fo.vm" template

The issue-navigator-fo.vm renders column widths based on customizable "weights". By default, the long Summary is set to the weight of 3, the super-long Description, Environment, Table Grid custom fields and all text-type custom fields are set to 5, while all other fields are set to 1:

#foreach($columnLayoutItem in $issueTableLayoutBean.columns)
	## select the proportional column width based on the field
	#set($columnWidth = "1")
	#set($fieldId = $columnLayoutItem.navigableField.id)
	#if($fieldId == "summary")
		#set($columnWidth = "3")
	#elseif($fieldId == "description" || $fieldId == "environment")
		#set($columnWidth = "5")
	#elseif($fieldId.contains("customfield_"))
		#set($customFieldTypeKey = $columnLayoutItem.navigableField.customFieldType.key)
		#if($customFieldTypeKey.contains(":text") || $customFieldTypeKey == "com.idalko.jira.plugins.igrid:tableGridCFType")
		## text type custom fields
			#set($columnWidth = "5")
		#end
	#end
	<fo:table-column column-width="proportional-column-width($columnWidth)"/>
#end

You can modify or extend this simple logic, as you wish.

Debugging templates

Tracing variables

We use a super-simple best practice to make our templates "debuggable", both while developing it and after they are deployed to production.

The idea is introducing a boolean variable ${debug} to control if some variables are shown (debugging time) or hidden (production time) in the exported PDF file:

#set($debug = false) ## set to true to display debug information

##	...

#if($debug)
	<fo:block>DEBUG: number of issues: ${issues.size()}</fo:block>
#end
#foreach($issue in $issues)
	#if($debug)
		<fo:block>DEBUG: issue key: ${issue.key}</fo:block>
	#end
	<fo:block>$xmlutils.escape($issue.summary)</fo:block>
	## ...
#end

The nicety here is that you can switch between the "debug" and "non-debug" modes easily and transparently even in a production Jira.

Logging from scripts

See the logging from scripts section in the Scripting page.

Debugging scripts

See the debugging scripts in Jira section in the Scripting page.

Questions?

Ask us any time.