Ралли пользовательских данных Cycle/Lead Time через Lookback API
Я пытаюсь сгенерировать пользовательские (и очень детальные) данные о цикле / времени выполнения для табличных представлений и диаграмм, используя REST API из JavaScript.
У меня есть собственное (необязательное) поле для моих историй и дефектов, c_KanbanStatus со следующими возможными значениями [ null, "Kickoff", "PO", "Creative", "Team Backlog", "Coding", "Acceptance Testing", "Принято" ].
Я только недавно добавил это настраиваемое поле, поэтому во многих моих историях это поле отсутствует (или не было "рождено" с ним).
Мое мышление выглядит следующим образом:
- Сделайте запрос назад для каждого перехода статуса Канбан
- Агрегировать по ObjectID
- Для каждого ObjectID, для каждого состояния вычислите разницу во времени между моментом, когда объект входит в это состояние, и тем, когда объект входит в последующее состояние.
Это выдержка из моего кода:
var kanbanStates =
[
"Kickoff",
"PO",
"Creative",
"Team Backlog",
"Coding",
"Acceptance Testing"
];
var username = "**************";
var password = "**************";
var deferreds = [];
for(var i = 0; i < kanbanStates.length; i++)
{
var find =
{
_ProjectHierarchy: ***************,
"_PreviousValues.c_KanbanStatus": { $lt: kanbanStates[i] },
c_KanbanStatus: kanbanStates[i]
};
var config =
{
url:"https://rally1.rallydev.com/analytics/v2.0/service/rally/workspace/********/artifact/snapshot/query.js?find=" + JSON.stringify(find) + "&fields=true&pagesize=999999",
dataType: "jsonp",
jsonp: "jsonp",
contentType: "application/json",
beforeSend: function(xhr)
{
xhr.setRequestHeader("Authorization", "Basic " + btoa(username + ":" + password));
}
};
deferreds.push($.ajax(config));
}
var aggregateResultsByObjectID = function(results)
{
var resultsByItemID = {};
for(var i = 0; i < results.length; i++)
{
if(!results[i][0].Results.c_KanbanStatus === kanbanStates[i])
throw "States don't match!";
for(var j = 0; j < results[i][0].Results.length; j++)
{
var itemID = results[i][0].Results[j].ObjectID;
if(!resultsByItemID.hasOwnProperty(itemID))
{
resultsByItemID[itemID] =
{
creationDate: results[i][0].Results[j].CreationDate,
name: results[i][0].Results[j].Name,
states: [],
results: []
};
}
resultsByItemID[itemID].results.push(results[i][0].Results[j]);
resultsByItemID[itemID].states.push(results[i][0].Results[j].c_KanbanStatus);
}
}
return resultsByItemID;
};
$.when.apply($, deferreds).done(function()
{
var resultsByItemID = aggregateResultsByObjectID(arguments);
console.log(resultsByItemID);
});
Проблема этого запроса заключается в том, что я получаю несколько результатов назад для каждого ObjectID для каждого состояния, хотя я указываю, что мне нужны только те снимки, которые имеют разные поля c_KanbanStatus. Когда я проверяю результаты для того же ObjectID и той же комбинации, если c_KanbanStatus и _PreviousValues.c_KanbanStatus, я получаю множество снимков назад, каждый с некоторым другим полем редактирования.
Например, для того же ObjectID я получаю эти два результата:
_PreviousValues:
{
_User: 10301773174
c_KanbanStatus: null
},
c_KanbanStatus: "Coding"
А ТАКЖЕ
_PreviousValues:
{
ScheduleState: 10148772688
_User: 10148977759
},
c_KanbanStatus: "Coding"
Я ожидаю, что первый результат (снимок для того, когда это пошло от не имеющего установленного поля c_KanbanStatus, к "Coding"). Второй результат, по-видимому, подразумевает, что он вообще не имеет поля c_KanbanStatus, а "кодирует", но почему?
У меня такое чувство, что мне не хватает чего-то глубоко в Lookback API. Пожалуйста, помогите мне понять!
2 ответа
Ожидается получить несколько результатов обратно для каждого состояния. Снимок создается каждый раз при изменении истории. Допустим, есть четыре состояния c_Kanban: backlog
, in-progress
, done
а также released
, Чтобы продолжить работу с этим примером, я переместил статью в столбец "В процессе" и установил PlanEstimate для истории, затем заблокировал задачу под этой историей, разблокировал задачу, установил состояние задачи "Завершено" и, наконец, переместил историю в колонку "Готово" в Канбан. доска. Все эти изменения будут иметь уникальные снимки, созданные в то время как c_Kanban === 'in-progress'
У меня есть приложение в этом репозитории github, которое создает сетку со столбцами для времен, проведенных историей в каждом состоянии c_Kanban. уведомление Blocked: true
в одном снимке и Blocked: false
в другое время c_Kanban: "in-progress"
:
Я возился со своим собственным решением этой проблемы, вот что я придумал. Требуется дополнительная настройка для обработки угловых случаев, но это близко к тому, что мне нужно. Я продолжу разработку и создам репозиторий GitHub для этого на случай, если кто-нибудь сможет использовать это.
РЕДАКТИРОВАТЬ: GitHub РЕПО: https://github.com/bfanti/RallyCustomCycleTimeApp
<!DOCTYPE html>
<html>
<head>
<title>My App</title>
<!--App information-->
<meta name="Name" content="App: Custom Cycle Time Table"/>
<meta name="Version" content="1.0"/>
<meta name="Vendor" content=""/>
<!--Include SDK-->
<script type="text/javascript" src="/apps/2.0rc1/sdk.js"></script>
<!--App code-->
<script type="text/javascript">
var kanbanStatuses =
[
"New Feature",
"Kickoff",
"PO",
"Creative",
"Team Backlog",
"Coding",
"Acceptance Testing"
];
var shirtSizeLUT =
{
3: "S",
5: "M",
8: "L"
};
Rally.onReady(function()
{
Ext.define("CustomApp",
{
extend: "Rally.app.App",
componentCls: "app",
launch: function()
{
var self = this;
Ext.create("Rally.data.lookback.SnapshotStore",
{
fetch : [ "_UnformattedID", "_TypeHierarchy", "Name", "PlanEstimate", "c_KanbanStatus", "ScheduleState" ],
hydrate : [ "c_KanbanStatus", "ScheduleState" ],
filters :
[
{
property : "_ProjectHierarchy",
value : ***********
},
{
property: "_TypeHierarchy",
value: { $nin: [ -51009, -51012, -51031, -51078 ] }
},
{
property: "_ValidFrom",
value: { $gt: "2013-09-13" }
}
],
sorters :
[
{
property : "_ValidTo",
direction : "ASC"
}
]
}).load(
{
params:
{
compress: false,
removeUnauthorizedSnapshots: true
},
callback : function(records, operation, success)
{
var aggregateCycleTimes = [];
var allObjectIDs = {};
Ext.Array.each(records, function(record) { allObjectIDs[record.get("ObjectID")] = record.get("ObjectID"); });
for(var storyIndex = 0; storyIndex < Object.keys(allObjectIDs).length; storyIndex++)
{
(function()
{
var currentObjectID = parseInt(Object.keys(allObjectIDs)[storyIndex], 10);
var recordsByStory = Ext.Array.filter(records, function(record) { return record.get("ObjectID") === currentObjectID; });
var currentStateOfStory = "";
var currentStateOfStorySnapshot = Ext.Array.filter(recordsByStory, function(record) { return record.get("_ValidTo").indexOf("9999") !== -1; })[0];
if(currentStateOfStorySnapshot)
currentStateOfStory = currentStateOfStorySnapshot.get("c_KanbanStatus");
var formattedID = (Ext.Array.contains(recordsByStory[0].get("_TypeHierarchy"), -51006) ? "DE" : "US") + recordsByStory[0].get("_UnformattedID");
var cycleTimes =
{
id: formattedID,
name: recordsByStory[0].get("Name"),
planEstimate: shirtSizeLUT[Rally.util.Array.last(recordsByStory).get("PlanEstimate")],
currentStateOfStory: currentStateOfStory
};
var allStatusesAreNull = true;
for(var i = 0; i < kanbanStatuses.length; i++)
{
var currentStatus = kanbanStatuses[i];
var currentSnapshot = Ext.Array.filter(recordsByStory, function(record) { return record.get("c_KanbanStatus") === currentStatus; })[0];
if(!currentSnapshot)
{
cycleTimes[currentStatus] = null;
continue;
}
var firstDate = new Date(currentSnapshot.get("_ValidFrom"));
var nextSnapshot = Rally.util.Array.last(Ext.Array.filter(recordsByStory, function(record) { return record.get("c_KanbanStatus") === currentStatus; }));
var secondDate = new Date();
if(nextSnapshot && (new Date(nextSnapshot.get("_ValidTo"))).getFullYear() !== 9998)
secondDate = new Date(nextSnapshot.get("_ValidTo"));
var cycleTime = Rally.util.DateTime.getDifference(secondDate, firstDate, "day");
cycleTimes[currentStatus] = cycleTime;
if(cycleTime !== null)
allStatusesAreNull = false;
}
if(!allStatusesAreNull)
aggregateCycleTimes.push(cycleTimes);
})();
}
var myStore = Ext.create("Rally.data.custom.Store",
{
data: aggregateCycleTimes,
pageSize: 100,
});
var columnConfig =
[
{
text: "ID",
dataIndex: "id"
},
{
text: "Name",
dataIndex: "name",
width: "280px"
},
{
text: "Size",
dataIndex: "planEstimate"
},
{
text: "Current State",
dataIndex: "currentStateOfStory"
}
];
for (var i = 0; i < kanbanStatuses.length; i++)
{
var columnConfigElement = {};
columnConfigElement["text"] = kanbanStatuses[i];
columnConfigElement["dataIndex"] = kanbanStatuses[i];
columnConfig.push(columnConfigElement);
}
if (!self.grid)
{
self.grid = self.add(
{
xtype: "rallygrid",
itemId: "mygrid",
store: myStore,
columnCfgs: columnConfig
});
}
}
});
}
});
Rally.launchApp("CustomApp", { name: "My Custom App" });
});
</script>
</head>
<body>
</body>
</html>