Name
global.BurnDownUtil
Description
Contains the functions needed for burn down chart creation.
Script
var BurnDownUtil = Class.create();
BurnDownUtil.RELEASE = "RELEASE";
BurnDownUtil.SPRINT = "SPRINT";
BurnDownUtil.RELEASE_STATE_DRAFT = -6;
BurnDownUtil.RELEASE_STATE_PLANNING = 1;
BurnDownUtil.RELEASE_STATE_CURRENT = 2;
BurnDownUtil.RELEASE_STATE_COMPLETE = 3;
BurnDownUtil.RELEASE_STATE_CANCELLED = 4;
BurnDownUtil.SPRINT_STATE_DRAFT = -6;
BurnDownUtil.SPRINT_STATE_PLANNING = 1;
BurnDownUtil.SPRINT_STATE_CURRENT = 2;
BurnDownUtil.SPRINT_STATE_COMPLETE = 3;
BurnDownUtil.SPRINT_STATE_CANCELLED = 4;
BurnDownUtil.STORY_STATE_TESTING = -8;
BurnDownUtil.STORY_STATE_READY_FOR_TESTING = -7;
BurnDownUtil.STORY_STATE_DRAFT = -6;
BurnDownUtil.STORY_STATE_BLOCKED = -5;
BurnDownUtil.STORY_STATE_READY = 1;
BurnDownUtil.STORY_STATE_WORK_IN_PROGRESS = 2;
BurnDownUtil.STORY_STATE_COMPLETE = 3;
BurnDownUtil.STORY_STATE_CANCELLED = 4;
BurnDownUtil.isComplete = function(state){
if (!state)
return false;
return state == BurnDownUtil.STORY_STATE_COMPLETE;
};
BurnDownUtil.isCancelled = function(state){
if (!state)
return false;
return state == BurnDownUtil.STORY_STATE_CANCELLED;
};
BurnDownUtil.isWorkInProgress = function(state){
if (!state)
return false;
return state == BurnDownUtil.STORY_STATE_WORK_IN_PROGRESS;
};
BurnDownUtil.isDraft = function(state){
if (!state)
return false;
return state == BurnDownUtil.STORY_STATE_DRAFT;
};
BurnDownUtil.isPlanning = function(state){
if (!state)
return false;
return state == BurnDownUtil.SPRINT_STATE_PLANNING;
};
BurnDownUtil.isReady = function(state){
return BurnDownUtil.isPlanning(state);
};
BurnDownUtil.prototype = {
initialize: function(recordSysId, burnDownType){
if (!recordSysId)
return;
this.sourceSysId = recordSysId;
this.burnDownType = null;
this.sourceRecord = null;
switch (burnDownType) {
case BurnDownUtil.RELEASE:
this.burnDownType = BurnDownUtil.RELEASE;
this.sourceRecord = this._getRecord("rm_release_scrum", recordSysId);
this.storyRefField = "release";
this.totalsMetric = BurnDownMetricUtil.RELEASE_METRIC;
break;
case BurnDownUtil.SPRINT:
default:
this.burnDownType = BurnDownUtil.SPRINT;
this.sourceRecord = this._getRecord("rm_sprint", recordSysId);
this.storyRefField = "sprint";
this.totalsMetric = BurnDownMetricUtil.SPRINT_METRIC;
}
var dates = this._calcStartAndEndDates();
this.startDate = dates.startDate;
this.endDate = dates.endDate;
},
isValid: function(){
return this.sourceRecord != null && this.sourceRecord.isValid();
},
isSprintBurnDown: function(){
return this.burnDownType == BurnDownUtil.SPRINT;
},
isReleaseBurnDown: function(){
return this.burnDownType == BurnDownUtil.RELEASE;
},
getTitle: function(){
return this.sourceRecord.number + ": " + this.sourceRecord.short_description;
},
getInitialDate: function(){
var initial = this.getStartDate();
initial.addDays(-1);
return initial.getDate();
},
getStartDate: function(){
return new GlideDateTime(this.startDate).getLocalDate();
},
getEndDate: function(){
return new GlideDateTime(this.endDate).getLocalDate();
},
// We don't manipulate the end date, so return the same instance
_getEndDate: function(){
return this.endDate.getLocalDate();
},
/**
*
* @param sprintSysId
* @returns [{startDate, dailyPoints}]
*/
getDailyPoints: function(){
var data = [];
var startDate = this.getStartDate();
var endDate = this._getEndDate();
// Grab all the metrics or the total amount of work to be done and
// chuck it into a "map" keyed date.
var sprintMetrics = this._getDailyPointsMap();
startDate.addDays(-1);
data.push({
key: new GlideDateTime(startDate).getDate(),
value: sprintMetrics.startingValue
});
startDate.addDays(1);
while (startDate.compareTo(endDate) <= 0) {
data.push({
key: new GlideDateTime(startDate).getDate(),
value: sprintMetrics[startDate].dailyPoints
});
startDate.addDays(1);
}
return data;
},
/**
* @returns {dailyPoints, changed} keyed on date
*/
_getDailyPointsMap: function(){
var data = {};
var startDate = this.getStartDate();
var endDate = this._getEndDate();
// Grab all the metrics or the total amount of work to be done and
// chuck it into a "map" keyed date.
var metrics = this._getDailyTotalMetrics();
var dailyPointsTotal = metrics.startingValue;
while (startDate.compareTo(endDate) <= 0) {
if (JSUtil.nil(metrics[startDate])) {
data[startDate] = {
dailyPoints: dailyPointsTotal,
changed: false
};
} else {
dailyPointsTotal = metrics[startDate];
data[startDate] = {
dailyPoints: dailyPointsTotal,
changed: true
};
}
startDate.addDays(1);
}
data.startingValue = metrics.startingValue;
return data;
},
getIdeal: function(){
var data = [];
var startDate = this.getStartDate();
var endDate = this._getEndDate();
var daysRemaining = (endDate.getNumericValue() - startDate.getNumericValue()) / 1000 / 3600 / 24 + 1;
// Grab all the metrics or the total amount of work to be done and
// chuck it into a "map" keyed date.
var dailyTotalsMap = this._getDailyPointsMap();
var dailyPointsTotal = dailyTotalsMap.startingValue;
var dailyAfterBurnDown = dailyPointsTotal;
var burnDownAmount = dailyPointsTotal / daysRemaining;
// Force initial value into series
startDate.addDays(-1);
data.push({
key: new GlideDateTime(startDate).getDate(),
value: dailyAfterBurnDown
});
startDate.addDays(1);
while (startDate.compareTo(endDate) <= 0) {
// Fill in any missing values between startDate and endDate
dailyAfterBurnDown += dailyTotalsMap[startDate].dailyPoints - dailyPointsTotal;
dailyPointsTotal = dailyTotalsMap[startDate].dailyPoints;
if (dailyTotalsMap[startDate].changed)
burnDownAmount = dailyAfterBurnDown / daysRemaining;
dailyAfterBurnDown -= burnDownAmount;
daysRemaining--;
data.push({
key: new GlideDateTime(startDate).getDate(),
value: Math.max(dailyAfterBurnDown, 0)
});
startDate.addDays(1);
}
return data;
},
getActual: function(){
var data = [];
// Grab all the metrics or the total amount of work to be done and
// chuck it into a "map" keyed date.
var dailyTotalsMap = this._getDailyPointsMap();
// Grab all the metrics or the closed stories
// and chuck it into a "map" keyed by date.
var startDate = this.getStartDate();
var endDate = this._getEndDate();
var today = new GlideDate().getLocalDate();
var storyMetrics = this._getStoryMetrics();
var points = 0;
// Force initial value into series
startDate.addDays(-1);
data.push({
key: new GlideDateTime(startDate).getDate(),
value: dailyTotalsMap.startingValue
});
startDate.addDays(1);
while (startDate.compareTo(endDate) <= 0 && startDate.compareTo(today) <= 0) {
points += storyMetrics[startDate] ? storyMetrics[startDate] : 0;
data.push({
key: new GlideDateTime(startDate).getDate(),
value: Math.max(dailyTotalsMap[startDate].dailyPoints + points, 0)
});
startDate.addDays(1);
}
return data;
},
hasMetricData: function(){
if (!this.isValid())
return false;
var gr = new GlideRecord("metric_instance");
gr.addQuery("id", this.sourceSysId);
gr.addQuery("definition.name", this.totalsMetric);
gr.query();
if (gr.next())
return true;
return false;
},
hasValidDates: function(){
if (!this.isValid())
return false;
return !this.sourceRecord.start_date || !this.sourceRecord.end_date ? false : true;
},
_getDailyTotalMetrics: function(){
var totals = {};
var gr = new GlideRecord("metric_instance");
gr.addQuery("id", this.sourceSysId);
gr.addQuery("definition.name", this.totalsMetric);
gr.orderBy("start");
gr.query();
if (gr.next()) {
if (gr.start.getGlideObject().getLocalDate().compareTo(this.getStartDate()) <= 0)
totals.startingValue = gr.value - 0;
else
totals.startingValue = 0;
do {
totals[gr.start.getGlideObject().getLocalDate()] = gr.value - 0;
} while (gr.next());
}
return totals;
},
_getStoryMetrics: function(){
var metrics = {};
var storyPoints = {};
var storySysIds = [];
var startDate = this.getStartDate();
var endDate = this._getEndDate();
// Get all the stories that have metric records created against them.
// Then put store the points value for later use.
var gr = new GlideRecord("rm_story");
gr.addQuery(this.storyRefField, this.sourceSysId);
gr.query();
while (gr.next()) {
storyPoints[String(gr.sys_id)] = gr.story_points - 0;
storySysIds.push(String(gr.sys_id));
}
// Retrieve all the metrics for the stories of the sprint
// and add the points stored in the map above
gr = new GlideRecord("metric_instance");
gr.addQuery("definition.name", BurnDownMetricUtil.STORY_METRIC);
gr.addQuery("id", "IN", storySysIds.join(","));
gr.orderBy("start");
gr.query();
// All metrics should begin with a completed or cancelled entry. If we encounter an entry that's not
// cancelled or completed first we should disregard it as it's more than likely a story that was cancelled
// in a previous sprint that's been moved to the current sprint.
var storyCarryOvers = {};
while (gr.next()) {
var date = gr.start.getGlideObject().getLocalDate();
var value = 0;
// Exclude any dates that are outside the sprint range
if (date.compareTo(startDate) < 0 || date.compareTo(endDate) > 0)
continue;
switch (gr.field_value - 0) {
case BurnDownUtil.STORY_STATE_CANCELLED:
if (!storyCarryOvers[String(gr.id)])
storyCarryOvers[String(gr.id)] = BurnDownUtil.STORY_STATE_CANCELLED;
break;
case BurnDownUtil.STORY_STATE_COMPLETE:
if (!storyCarryOvers[String(gr.id)])
storyCarryOvers[String(gr.id)] = BurnDownUtil.STORY_STATE_CANCELLED;
value = storyPoints[gr.id] * -1;
break;
default:
if (storyCarryOvers[String(gr.id)])
value = storyPoints[gr.id];
}
metrics[date] = metrics[date] ? metrics[date] + value : value;
}
return metrics;
},
_calcStartAndEndDates: function(){
var startDate = this.sourceRecord.start_date.getGlideObject();
var endDate = this.sourceRecord.end_date.getGlideObject();
return {
startDate: startDate,
endDate: endDate
};
},
_getRecord: function(tableName, sysId){
var gr = new GlideRecord(tableName);
if (gr.get(sysId))
return gr;
return null;
},
type: 'BurnDownUtil'
};
Sys ID
c7409b7237131000913e40ed9dbe5d6f