Hi everyone!
I created a custom JQL function in Jira Data Center using ScriptRunner.
The function allows users to search issues by the quarter in which they were created.
Example:
issueFunction in createdQuarter(1)
This returns issues created in the first quarter of the current year.
The function uses the current year automatically and converts the selected quarter into the correct created date range.
This function is especially useful for dashboards that show quarterly data. Since it automatically uses the current year, there is no need to manually update the JQL date ranges every year.
Script
package com.onresolve.jira.groovy.jql
import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.jql.parser.JqlQueryParser
import com.atlassian.jira.jql.query.LuceneQueryBuilder
import com.atlassian.jira.jql.query.QueryCreationContext
import com.atlassian.jira.jql.validator.NumberOfArgumentsValidator
import com.atlassian.jira.user.ApplicationUser
import com.atlassian.jira.util.MessageSet
import com.atlassian.query.clause.TerminalClause
import com.atlassian.query.operand.FunctionOperand
import org.apache.lucene.search.Query
import java.time.LocalDate
class CreatedQuarter extends AbstractScriptedJqlFunction implements JqlQueryFunction {
JqlQueryParser queryParser = ComponentAccessor.getComponent(JqlQueryParser)
LuceneQueryBuilder luceneQueryBuilder = ComponentAccessor.getComponent(LuceneQueryBuilder)
@Override
String getDescription() {
"Returns issues created in the selected quarter of the current year. Example: issueFunction in createdQuarter(1)"
}
@Override
List<Map> getArguments() {
[
[
description: "Quarter number: 1, 2, 3, or 4",
optional : false,
]
]
}
@Override
String getFunctionName() {
"createdQuarter"
}
@Override
MessageSet validate(ApplicationUser user, FunctionOperand operand, TerminalClause terminalClause) {
def messageSet = new NumberOfArgumentsValidator(1, 1, getI18n()).validate(operand)
if (messageSet.hasAnyErrors()) {
return messageSet
}
def quarterText = operand.args.first()
if (!(quarterText ==~ /[1-4]/)) {
messageSet.addErrorMessage(
"createdQuarter() accepts only 1, 2, 3, or 4. Example: issueFunction in createdQuarter(1)"
)
return messageSet
}
def query = buildJiraQuery(quarterText as Integer)
searchService.validateQuery(user, query)
}
@Override
Query getQuery(QueryCreationContext queryCreationContext, FunctionOperand operand, TerminalClause terminalClause) {
def quarter = operand.args.first() as Integer
def query = buildJiraQuery(quarter)
luceneQueryBuilder.createLuceneQuery(queryCreationContext, query.whereClause)
}
private com.atlassian.query.Query buildJiraQuery(Integer quarter) {
int year = LocalDate.now().year
LocalDate startDate
LocalDate endDate
switch (quarter) {
case 1:
startDate = LocalDate.of(year, 1, 1)
endDate = LocalDate.of(year, 4, 1)
break
case 2:
startDate = LocalDate.of(year, 4, 1)
endDate = LocalDate.of(year, 7, 1)
break
case 3:
startDate = LocalDate.of(year, 7, 1)
endDate = LocalDate.of(year, 10, 1)
break
case 4:
startDate = LocalDate.of(year, 10, 1)
endDate = LocalDate.of(year + 1, 1, 1)
break
default:
throw new IllegalArgumentException("Quarter must be 1, 2, 3, or 4")
}
def jql = "created >= '${startDate}' AND created < '${endDate}'"
queryParser.parseQuery(jql)
}
}
This corrected ScriptRunner code for Jira Data Center fixes validation errors by implementing the correct JqlFunction interface and properly defining the searchService component to enable filtering by the current year's quarters. The revised script, available below, also updates the date handling to LocalDate.now().getYear() for better compatibility.
package com.onresolve.jira.groovy.jql
import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.jql.parser.JqlQueryParser
import com.atlassian.jira.jql.query.LuceneQueryBuilder
import com.atlassian.jira.jql.query.QueryCreationContext
import com.atlassian.jira.jql.validator.NumberOfArgumentsValidator
import com.atlassian.jira.plugin.jql.function.JqlFunction // Fixed import
import com.atlassian.jira.user.ApplicationUser
import com.atlassian.jira.util.MessageSet
import com.atlassian.query.clause.TerminalClause
import com.atlassian.query.operand.FunctionOperand
import org.apache.lucene.search.Query
import java.time.LocalDate
class CreatedQuarter extends AbstractScriptedJqlFunction implements JqlFunction { // Fixed interface
JqlQueryParser queryParser = ComponentAccessor.getComponent(JqlQueryParser)
LuceneQueryBuilder luceneQueryBuilder = ComponentAccessor.getComponent(LuceneQueryBuilder)
// Resolved missing searchService reference
def searchService = ComponentAccessor.getComponent(com.atlassian.jira.bc.issue.search.SearchService)
@Override
String getDescription() {
"Returns issues created in the selected quarter of the current year. Example: issueFunction in createdQuarter(1)"
}
@Override
List<Map> getArguments() {
[
[
description: "Quarter number: 1, 2, 3, or 4",
optional : false,
]
]
}
@Override
String getFunctionName() {
"createdQuarter"
}
@Override
MessageSet validate(ApplicationUser user, FunctionOperand operand, TerminalClause terminalClause) {
def messageSet = new NumberOfArgumentsValidator(1, 1, getI18n()).validate(operand)
if (messageSet.hasAnyErrors()) {
return messageSet
}
def quarterText = operand.args.first()
if (!(quarterText ==~ /[1-4]/)) {
messageSet.addErrorMessage(
"createdQuarter() accepts only 1, 2, 3, or 4. Example: issueFunction in createdQuarter(1)"
)
return messageSet
}
def query = buildJiraQuery(quarterText as Integer)
searchService.validateQuery(user, query)
}
@Override
Query getQuery(QueryCreationContext queryCreationContext, FunctionOperand operand, TerminalClause terminalClause) {
def quarter = operand.args.first() as Integer
def query = buildJiraQuery(quarter)
luceneQueryBuilder.createLuceneQuery(queryCreationContext, query.whereClause)
}
private com.atlassian.query.Query buildJiraQuery(Integer quarter) {
int year = LocalDate.now().getYear() // Idiomatic Java Time syntax
LocalDate startDate
LocalDate endDate
switch (quarter) {
case 1:
startDate = LocalDate.of(year, 1, 1)
endDate = LocalDate.of(year, 4, 1)
break
case 2:
startDate = LocalDate.of(year, 4, 1)
endDate = LocalDate.of(year, 7, 1)
break
case 3:
startDate = LocalDate.of(year, 7, 1)
endDate = LocalDate.of(year, 10, 1)
break
case 4:
startDate = LocalDate.of(year, 10, 1)
endDate = LocalDate.of(year + 1, 1, 1)
break
default:
throw new IllegalArgumentException("Quarter must be 1, 2, 3, or 4")
}
def jql = "crea
ted >= '${startDate}' AND created < '${endDate}'"
queryParser.parseQuery(jql)
}
}
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.