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;