JIRA has a built-in Due Date field. But what happens when you don't know the exact day – you know, at best, the year quarter (Q1–Q4) in which the issue is due.
A client of ours solved this by defining a custom field, showing the quarter (and optionally, year) to which the issue is committed to be delivered in:
This works, but the result is a little hard for humans to parse, as there is very little visual distinction between different values:
Solution – an easy-to-visualize 'view'
We solved this by creating a read-only graphical "timeline" view of the field, here seen alongside the text view:
I think you'll agree, it's much easier to see see what's going on. The field begins with the current quarter (it's May 2016 at time of writing, so Q2), and displays 4 quarters into the future. The last issue, with the grey ellipsis, shows an issue committed to before the current quarter (i.e. overdue). A mouseover explains what's going on:
Likewise for issues committed to more than one year in the future.
Implementation
We utilized the indispensable ScriptRunner for JIRA plugin, and created a 'script field', i.e. a field whose value is calculated programmatically. The implementation is as follows:
/**
* An alternative timeline view of the 'Quarter Commit' field.
* jeff@redradishtech.com, 4/May/16.
*/
import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.issue.customfields.option.Option
import com.atlassian.jira.issue.fields.CustomField
// ID of the cascading select custom field storing the Quarter and Year.
def QUARTER_COMMIT_CUSTOMFIELDID = 14100;
/** Get the year in which a customfield was last set in this issue, or null if never set. */
def Integer getFieldSetYear(CustomField customField) {
def chm = ComponentAccessor.getChangeHistoryManager();
def historyItems = chm.getChangeItemsForField(issue, customField.getName());
if (historyItems.empty) return null;
fieldSetDate = historyItems.reverse().first().getCreated();
def fieldSetCal = Calendar.getInstance();
fieldSetCal.setTimeInMillis(fieldSetDate.getTime());
year = fieldSetCal.get(Calendar.YEAR)
return year;
}
cf = ComponentAccessor.customFieldManager.getCustomFieldObject(QUARTER_COMMIT_CUSTOMFIELDID);
Map<String, Option> cfVal = issue.getCustomFieldValue(cf);
if (!cfVal) return;
currentYear = Calendar.getInstance().get(Calendar.YEAR) as Integer;
currentQuarter = Calendar.getInstance().get(Calendar.MONTH).intdiv(3) as Integer; // 0 to 3
issueQuarter = cfVal[null]?.getValue()?.replaceFirst("[qQ]", "")?.toInteger() - 1; // 0 to 3. E.g. "Q1" becomes 0
issueYear = cfVal["1"]?.getValue()?.toInteger(); // e.g. 2017
if (!issueYear) {
// It's possible for the field's year to be unset. In that case we cunningly infer the year from the
// date at which 'Quarter Commit' was last modified.
issueYear = getFieldSetYear(cf);
if (!issueYear) {
// If for some bizarre reason we can't tell when Quarter Commit was set, default to current year.
issueYear = currentYear
};
};
QUARTERS = ["Q1", "Q2", "Q3", "Q4"]
/**
* Display the 'before current quarter' block, grey if our issue was scheduled any time
* before the current quarter, white otherwise.
**/
def beforeNowBlock() {
if ((issueYear < currentYear) ||
(issueYear == currentYear && issueQuarter < currentQuarter))
{
"<th style='padding-left: 0px; padding-right: 0px; background: lightgrey' title='Committed to Q${issueQuarter+1} ${issueYear?issueYear:''} (in the past)'>…</th>"
} else {
"<td style='padding-left: 0px; padding-right: 0px;'>…</td>"
}
}
/**
* Display a quarter's block. If the issue's year + quarter equals ours, display green, otherwise white.
*
* @param quarterOffset Display the n'th from current quarter (0 to 3). I.e. 0 means "this quarter", 1 means "next qurater" etc.
*
*/
def quarterBlock(quarterOffset) {
def q = QUARTERS[quarterOffset % 4]
// If the year is unset (implying current), or is equal to our year, display just the quarter. Otherwise display quarter plus year.
// log.error("Considering ${q}: does issue year ${issueYear} equal ${currentYear + ((quarterOffset).intdiv(4))}, and does issue quarter ${issueQuarter} equal our quarter ${quarterOffset}?")
output = (issueYear == currentYear + ((quarterOffset).intdiv(4)) && issueQuarter == quarterOffset%4) ?
"<th style='background-color:lightgreen' title='Committed to Q${issueQuarter+1} ${issueYear}'>${q}</th>" :
"<td style='color:grey'>${q}</td>";
output
}
/* Display the 'after our 4-quarter window' block. */
def afterQuartersBlock() {
// E.g. for Q1 2017 when we're in Q2, don't show (currentQuarter=1, issueQuarter=0)
if ((issueYear > currentYear) &&
issueQuarter >= currentQuarter) {
"<th style='padding-left: 0px; padding-right: 0px; background-color: lightgreen' title='Committed to Q${issueQuarter+1} ${issueYear}'>…</th>"
} else {
"<td style='padding-left: 0px; padding-right: 0px;'>…</td>"
}
}
return """
<table class="aui">
<tbody>
<tr>
${beforeNowBlock()}
${quarterBlock(currentQuarter)}
${quarterBlock(currentQuarter+1)}
${quarterBlock(currentQuarter+2)}
${quarterBlock(currentQuarter+3)}
${afterQuartersBlock()}
</tr>
</tbody>
</table>
""" as String;



