How to get status of portfolio saga item by story hours? - rally

Does anyone know an easy way to get a percent done by story hours related to 'Portfolioitem/Saga'? The existing portfolio items app in CA only shows percent done by story plan estimate and I'd like to get the task hours details as well.
I've written the following custom app to include the data but my table gets loaded before the data is returned. Please help!
Ext.define('Rally.app.PortfolioGrid', {
extend: 'Rally.app.App',
componentCls: 'app',
launch: function() {
console.log('Working');
this._loadComboBox();
},
_loadComboBox : function() {
this.searchComboBox = Ext.create('Rally.ui.combobox.ArtifactSearchComboBox', {
fieldLabel: 'Saga',
noEntryText: '',
emptyText: 'Enter saga ID or keyword...',
//grow: true,
//allowNoEntry: false,
storeConfig: {
autoLoad: true,
models: ['PortfolioItem/Saga']
},
listeners: {
select: function(combobox, records) {
this._loadData();
},
scope: this
}
});
this.add(this.searchComboBox);
},
_loadData: function () {
var selectedID = this.searchComboBox.getRecord().get('FormattedID');
console.log('Selected Saga', selectedID);
var myFilters = [
{
property: 'FormattedID',
operation: '=',
value: selectedID
},
];
if (this.myStore) {
console.log('store exists');
this.myStore.setFilter(myFilters);
this.myStore.load();
// create store
} else {
this.myStore = Ext.create('Rally.data.wsapi.Store', {
model: 'PortfolioItem/Saga',
autoLoad: true,
fetch: ['FormattedID', 'Name', 'Children', 'Release', 'State', 'PercentDoneByStoryPlanEstimate', 'PercentDoneByStoryCount', 'Project', 'Owner'],
filters: myFilters,
listeners: {
load: this._onDataLoaded,
scope: this
}
});
}
},
_onDataLoaded: function(store, data){
var features = [];
var pendingstories = data.length;
Ext.Array.each(data, function(feature) {
var f = {
FormattedID: feature.get('FormattedID'),
Name: feature.get('Name'),
_ref: feature.get("_ref"),
State: (feature.get('State') && feature.get('State').Name) || ' ',
Project: (feature.get('Project') && feature.get('Project').Name) || ' ',
Owner: (feature.get('Owner') && feature.get('Owner')._refObjectName) || 'No Owner',
PercentDoneByStoryPlanEstimate: Math.floor(feature.get('PercentDoneByStoryPlanEstimate') * 100) + '%',
PercentDoneByStoryCount: Math.floor(feature.get('PercentDoneByStoryCount') * 100) + '%',
Children: [],
totalPlannedHours: 0,
totalHoursRemaining: 0,
totalHoursCompleted: 0,
Percentage: 0
};
var sagaFeatures = feature.getCollection('Children');
sagaFeatures.load({
fetch: ['FormattedID', 'Parent', 'UserStories', 'DirectChildrenCount'],
callback: function(records, operation, success){
Ext.Array.each(records, function(child){
var s = {
FormattedID: child.get('FormattedID'),
};
if (child.get('DirectChildrenCount') > 0) {
child.getCollection('UserStories').load({
fetch: ['FormattedID', 'TaskEstimateTotal', 'TaskRemainingTotal'],
callback: function(records, operation, success) {
Ext.Array.each(records, function(us) {
f.totalPlannedHours += us.get('TaskEstimateTotal');
f.totalHoursRemaining += us.get('TaskRemainingTotal');
f.totalHoursCompleted = f.totalPlannedHours - f.totalHoursRemaining
console.log("Total Hours Completed", f.totalHoursRemaining)
f.Percentage = Math.floor((f.totalHoursCompleted / f.totalPlannedHours) * 100) + '%';
}, this);
}, scope: this});
}
f.Children.push(s);
}, this);
},
scope: this
});
features.push(f);
--pendingstories;
if (pendingstories === 0) {
this._createGrid(features);
}
}, this);
this._createGrid(features);
},
_createGrid: function(features) {
var myCustomStore = Ext.create('Rally.data.custom.Store', {
data: features
//pageSize: 100,
});
if (!this.grid) {
this.grid = this.add({
xtype: 'rallygrid',
flex: 1,
store: myCustomStore,
columnCfgs: [
{
text: 'Formatted ID', dataIndex: 'FormattedID', xtype: 'templatecolumn',
tpl: Ext.create('Rally.ui.renderer.template.FormattedIDTemplate')
},
{
text: 'Name', dataIndex: 'Name'
},
{
text: 'State', dataIndex: 'State'
},
{
text: 'Saga Features', dataIndex: 'Children',
renderer: function(value) {
var html = [];
Ext.Array.each(value, function(child){
html.push('' + child.FormattedID + '')
});
return html.join(', ');
}
},
{
text: 'Percent Done By Story Plan Estimate', dataIndex: 'PercentDoneByStoryPlanEstimate'
},
{
text: 'Percent Done By Story Count', dataIndex: 'PercentDoneByStoryCount'
},
{
text: 'Total Planned Hours', dataIndex: 'totalPlannedHours'
},
{
text: 'Total Completed Hours', dataIndex: 'totalHoursCompleted'
},
{
text: 'Percent Done by Story Hours', dataIndex: 'Percentage'
},
{
text: 'Project', dataIndex: 'Project'
},
{
text: 'Owner', dataIndex: 'Owner'
}
]
});
}
else {
this.grid.reconfigure(myCustomStore);
}
}
});

Can you load the data from the bottom up instead and save yourself all the many nested loops and store loads?
I think you should be able to just create a store of stories beneath the selected saga...
this.myStore = Ext.create('Rally.data.wsapi.Store', {
model: 'UserStory',
autoLoad: true,
limit: Infinity,
pageSize: 2000,
fetch: ['FormattedID', 'Name', 'TaskEstimateTotal', 'TaskRemainingTotal', 'Feature', 'Parent', 'Children'],
filters: [{ property: 'Feature.Parent', operator: '=', value: this.searchComboBox.getRecord().get('_ref')}],
listeners: {
load: this._onDataLoaded,
scope: this
}
});
And then just crunch your data into the appropriate data structure, bucketing them by Feature. Something like this?
_onDataLoaded: function(store) {
var sagas = {};
var stories = store.getRange();
_.each(stories, function(story) {
var feature = store.get('Feature'),
saga = feature.Parent;
if (!sagas[saga._ref]) {
sagas[saga._ref] = Ext.clone(saga);
}
var featureChildren = sagas[saga._ref].Children.values = sagas[saga._ref].Children.values || {};
if (!featureChildren[feature._ref]) {
featureChildren[feature._ref] = Ext.clone(feature);
}
var storyChildren = featureChildren[feature._ref].UserStories.values = featureChildren[feature._ref].UserStories.values || {};
if (!storyChildren[story._ref]) {
storyChildren[story._ref] = Ext.clone(story);
}
});
//now you have an object of sagas, just needs to be an array for your custom store data
var customStoreData = _.values(sagas);
this._createGrid(customStoreData);
}
Something like that should get you close. You'll need to move around your code a little bit where you're calculating your rollup totals, and your renderer for the Children column will be a tiny bit different- just loop over _.values(value.values) instead.

Related

Loading a Rally user story model including a the ObjectID's of the tasks associated with it

This piece of code used to work, it loads a User Story model including a summary (a list) of all of the tasks attached to it:
storyModel.load(story.id, {
fetch: ['Name', 'PlanEstimate', ‘Tasks:summary[ObjectID]’],
callback: function(result, operation) {
// result.get('Summary').Tasks.ObjectID return an object like this {
// https://rally1.rallydev.com/slm/webservice/v2.0//7613899947: 1,
// https://rally1.rallydev.com/slm/webservice/v2.0//7626630531: 1,
// …
// }
},
…
});
Now, result.get('Summary').Tasks.ObjectID returns no object at all.
Is there another way to get all the tasks attached to a given user story?
A full example of getting all tasks associated with a user story is available from this github repo. This code may do a little more than you need: it loads tasks of both stories and defects, in iteration, further filtered by the owner of the tasks.
var taskCollection = story.getCollection('Tasks',{fetch: ['Name', 'FormattedID', 'State', 'Owner']});
taskCollection.load({
callback: function(records, operation, success){
_.each(records, function(task){
var owner = (task.get('Owner') && task.get('Owner')._refObjectName) || '-- No Entry --';
if (owner===that._selectedUser) {
//only show tasks owned by the selected user
tasks.push(task);
}
});
That code uses promises. For a callbacks-only example, which builds a grid of user stories and associated tasks see a code in this repo. Both examples use AppSDK2rc2.
Ext.define('CustomApp', {
extend: 'Rally.app.TimeboxScopedApp',
componentCls: 'app',
scopeType: 'iteration',
comboboxConfig: {
fieldLabel: 'Select an Iteration:',
labelWidth: 100,
width: 300
},
onScopeChange: function() {
Ext.create('Rally.data.WsapiDataStore', {
model: 'UserStory',
fetch: ['FormattedID','Name','Tasks'],
pageSize: 100,
autoLoad: true,
filters: [this.getContext().getTimeboxScope().getQueryFilter()],
listeners: {
load: this._onDataLoaded,
scope: this
}
});
},
_createGrid: function(stories) {
var myStore = Ext.create('Rally.data.custom.Store', {
data: stories,
pageSize: 100,
});
if (!this.grid) {
this.grid = this.add({
xtype: 'rallygrid',
itemId: 'mygrid',
store: myStore,
columnCfgs: [
{
text: 'Formatted ID', dataIndex: 'FormattedID', xtype: 'templatecolumn',
tpl: Ext.create('Rally.ui.renderer.template.FormattedIDTemplate')
},
{
text: 'Name', dataIndex: 'Name'
},
{
text: 'Task Count', dataIndex: 'TaskCount'
},
{
text: 'Tasks', dataIndex: 'Tasks',
renderer: function(value) {
var html = [];
Ext.Array.each(value, function(task){
html.push('' + task.FormattedID + '')
});
return html.join(', ');
}
}
]
});
}else{
this.grid.reconfigure(myStore);
}
},
_onDataLoaded: function(store, data){
var stories = [];
var pendingTasks = data.length;
Ext.Array.each(data, function(story) {
var s = {
FormattedID: story.get('FormattedID'),
Name: story.get('Name'),
_ref: story.get("_ref"),
TaskCount: story.get('Tasks').Count,
Tasks: []
};
var tasks = story.getCollection('Tasks');
tasks.load({
fetch: ['FormattedID'],
callback: function(records, operation, success){
Ext.Array.each(records, function(task){
s.Tasks.push({_ref: task.get('_ref'),
FormattedID: task.get('FormattedID')
});
}, this);
--pendingTasks;
if (pendingTasks === 0) {
this._createGrid(stories);
}
},
scope: this
});
stories.push(s);
}, this);
}
});

Report to display all top level stories or portlio items in blocked status and include blocked reason

I would love a report that would display all top level user stories (or better yet portfolio items, if possible) that have one or more tasks in blocked status, with the blocked reasons included in the report. This would be across an entire release
I am thinking that many others would also want to view data like this as well, however I couldn't find this in any of the canned reports. Anyone familiar with a way to do this?
Thanks
Here is a custom app that builds a grid of blocked tasks with their workproducts. When a workproduct has a epic (parent) story and a parent feature, those are also shown in respective columns.
The full code is available in this github repo. You may copy/paste the html file into a custom page in Rally.
The js source:
Ext.define('CustomApp', {
extend: 'Rally.app.TimeboxScopedApp',
componentCls: 'app',
scopeType: 'iteration',
comboboxConfig: {
labelWidth: 100,
width: 300
},
onScopeChange: function() {
var filter = this.getContext().getTimeboxScope().getQueryFilter();
filter = filter.and({
property: 'Blocked',
value: true
});
filter.toString();
Ext.create('Rally.data.WsapiDataStore', {
model: 'Task',
fetch: ['ObjectID', 'FormattedID','Name', 'WorkProduct','Blocked', 'BlockedReason', 'Parent', 'Feature'],
autoLoad: true,
filters: [filter],
listeners: {
load: this._onDataLoaded,
scope: this
}
});
},
_onDataLoaded: function(store, data){
var tasks = [];
Ext.Array.each(data, function(task) {
console.log('Blocked',task.get('Blocked'));
console.log(task.get('WorkProduct')._type);
var t = {
ObjectID: task.get('ObjectID'),
FormattedID: task.get('FormattedID'),
Name: task.get('Name'),
_ref: task.get('_ref'),
WorkProduct: task.get('WorkProduct'),
Blocked: task.get('Blocked'),
BlockedReason: task.get('BlockedReason'),
WorkProductType: task.get('WorkProduct')._type
};
tasks.push(t);
}, this);
this._createGrid(tasks);
},
_createGrid: function(tasks) {
var that = this;
var myStore = Ext.create('Rally.data.custom.Store', {
data: tasks,
pageSize: 100,
});
if (!this.grid) {
this.grid = this.add({
xtype: 'rallygrid',
store: myStore,
columnCfgs: [
{
text: 'Formatted ID', dataIndex: 'FormattedID', xtype: 'templatecolumn',
tpl: Ext.create('Rally.ui.renderer.template.FormattedIDTemplate')
},
{
text: '', dataIndex: 'Name'
},
{
text: 'WorkProduct', dataIndex: 'WorkProduct',
renderer: function(val, meta, record) {
var type;
if(record.get('WorkProduct')._type === "HierarchicalRequirement") {
type = 'userstory';
}
else if(record.get('WorkProduct')._type === "Defect"){
type = 'defect';
}
return '' + record.get('WorkProduct').FormattedID + '';
}
},
{
text: 'Blocked Reason', dataIndex: 'BlockedReason'
},
{
text: 'Parent of WorkProduct', dataIndex: 'WorkProduct',
renderer: function(val, meta, record) {
if(record.get('WorkProduct')._type !== "HierarchicalRequirement") {
return 'n/a'
}
else{
return '' + record.get('WorkProduct').Parent.FormattedID + '';
}
}
},
{
text: 'Feature of WorkProduct', dataIndex: 'WorkProduct',
renderer: function(val, meta, record) {
if(record.get('WorkProduct')._type !== "HierarchicalRequirement") {
return 'n/a'
}
else{
return '' + record.get('WorkProduct').Feature.FormattedID + '';
}
}
}
]
});
}else{
this.grid.reconfigure(myStore);
}
}
});

Detailed view for each test set in a Rally project

I am trying to figure out a way to have a Rally summary page displaying all test sets per the globally chosen project and particulary the exact numbers of pass/totals per test set.
I can display this using TestCaseStatus but the strings returned are not what I want. I read through some posts and it seems that the only way to get this kind of details is to iterate through all test set test cases and check if they are passing or not on the client side; also to count them up.
Can anyone provide a working example of how to iterate through the test set test cases and count their last verdict BUT only for the current project not the last verdict in general?
An example of a custom add that displays a grid of Test Sets based on a Project selection and then displays a grid of associated test cases and their test case results when a user double clicks on a row in a first grid is available in this github repo. You may copy the html file to a custom html page you create for this purpose.
Here is a js file:
Ext.define('CustomApp', {
extend: 'Rally.app.App',
componentCls: 'app',
launch: function() {
var panel = Ext.create('Ext.panel.Panel', {
layout: 'hbox',
itemId: 'parentPanel',
componentCls: 'panel',
items: [
{
xtype: 'rallyprojectpicker',
fieldLabel: 'select project',
listeners:{
change: function(combobox){
if ( this.down('#g')) {
console.log('grid exists');
Ext.getCmp('g').destroy();
console.log('grid deleted');
}
this.onProjectSelected(combobox.getSelectedRecord());
},
scope: this
}
},
{
xtype: 'panel',
title: 'Test Sets',
itemId: 'childPanel1'
},
{
xtype: 'panel',
title: 'Test Cases',
width: 600,
itemId: 'childPanel2'
}
],
});
this.add(panel);
},
onProjectSelected:function(record){
var project = record.data['_ref'];
console.log('project', project);
var testSetStore = Ext.create('Rally.data.WsapiDataStore', {
model: 'TestSet',
fetch: ['FormattedID','Name', 'Project', 'TestCaseStatus', 'TestCases'],
pageSize: 100,
autoLoad: true,
filters: [
{
property: 'Project',
value: project
}
],
listeners: {
load: this.onTestSetsLoaded,
scope: this
}
});
},
onTestSetsLoaded:function(store, data){
var testSets = [];
Ext.Array.each(data, function(testset) {
var ts = {
FormattedID: testset.get('FormattedID'),
_ref: testset.get('_ref'),
Name: testset.get('Name'),
TestCaseCount: testset.get('TestCases').Count,
TestCaseStatus: testset.get('TestCaseStatus')
};
testSets.push(ts);
});
this.updateGrid(testSets);
},
updateGrid: function(testSets){
if (this.down('#g2')) {
console.log('g2 exists');
var store = this.down('#g2').getStore();
store.removeAll();
}
var store = Ext.create('Rally.data.custom.Store', {
data: testSets,
pageSize: 100
});
if (!this.down('#g')) {
this.createGrid(store);
}
else{
this.down('#g').reconfigure(store);
}
},
createGrid: function(store){
console.log("load grid", store);
var that = this;
var g = Ext.create('Rally.ui.grid.Grid', {
id: 'g',
store: store
});
var g = Ext.create('Rally.ui.grid.Grid', {
id: 'g',
store: store,
columnCfgs: [
{
text: 'Formatted ID', dataIndex: 'FormattedID', xtype: 'templatecolumn',
tpl: Ext.create('Rally.ui.renderer.template.FormattedIDTemplate')
},
{
text: 'Name', dataIndex: 'Name'
},
{
text: 'Test Case Count', dataIndex: 'TestCaseCount',
},
{
text: 'TestCaseStatus', dataIndex: 'TestCaseStatus'
}
],
listeners: {
celldblclick: function( grid, td, cellIndex, record, tr, rowIndex){
var id = grid.getStore().getAt(rowIndex).get('FormattedID');
console.log('id', id);
that.getTestCases(id);
}
}
});
this.down('#childPanel1').add(g);
},
getTestCases:function(id){
var selectedTestSetStore = Ext.create('Rally.data.WsapiDataStore', {
model: 'TestSet',
fetch: ['FormattedID','Name', 'TestCases'],
pageSize: 100,
autoLoad: true,
filters: [
{
property: 'FormattedID',
operator: '=',
value: id
}
],
listeners: {
load: this.onSelectedTestSetLoaded,
scope: this
}
});
},
onSelectedTestSetLoaded:function(store, data){
console.log('store',store);
console.log('data',data);
var selectedTestSets = [];
var pendingTestCases = data.length;
if (data.length ===0) {
this.createTestSetGrid(selectedTestSets);
}
Ext.Array.each(data, function(selectedTestset){
var ts = {
FormattedID: selectedTestset.get('FormattedID'),
TestCaseCount: selectedTestset.get('TestCases').Count,
TestCases: [],
ResultCount: 0
};
var testCases = selectedTestset.getCollection('TestCases', {fetch: ['FormattedID','ObjectID', 'Results']});
console.log('testCases:', selectedTestset.get('TestCases').Count, testCases);
testCases.load({
callback: function(records, operation, success){
Ext.Array.each(records, function(testcase){
console.log("testcase.get('FormattedID')", testcase.get('FormattedID'));
console.log("testcase.get('Results').Count", testcase.get('Results').Count);
ts.ResultCount = testcase.get('Results').Count;
console.log('ts.ResultCount', ts.ResultCount);
ts.TestCases.push({_ref: testcase.get('_ref'),
FormattedID: testcase.get('FormattedID'),
ObjectID: testcase.get('ObjectID')
});
}, this);
--pendingTestCases;
if (pendingTestCases === 0) {
this.makeTestCaseStore(ts.TestCases);
}
},
scope: this
});
console.log('ts', ts);
selectedTestSets.push(ts);
},this);
},
makeTestCaseStore:function(testcases){
console.log('makeTestCaseStore'); //ok
if (testcases.length>0) {
var idArray = [];
_.each(testcases, function(testcase){
console.log(testcase);
console.log('OID', testcase['ObjectID']);
idArray.push(testcase['ObjectID']);
});
console.log('idArray',idArray);
var filterArray = [];
_.each(idArray, function(id){
filterArray.push(
{
property: 'ObjectID',
value:id
}
)
});
console.log('filterArray', filterArray); //ok
var filters = Ext.create('Rally.data.QueryFilter', filterArray[0]);
filterArray = _.rest(filterArray,1);
_.each(filterArray, function(filter){
filters = filters.or(filter)
},1);
var testCaseStore = Ext.create('Rally.data.WsapiDataStore', {
model: 'TestCase',
fetch: ['FormattedID','Name', 'ObjectID', 'Results'],
pageSize: 100,
autoLoad: true,
filters: [filters],
listeners: {
load: this.onTestCasesLoaded,
scope: this
}
});
}
else{
if (this.down('#g2')) {
var store = this.down('#g2').getStore();
store.removeAll();
}
}
},
onTestCasesLoaded:function(store,data){
console.log('onTestCasesLoaded');
console.log('store',store);
console.log('data',data);
var testCases = [];
var pendingResults = data.length;
Ext.Array.each(data, function(testcase) {
var tc = {
FormattedID: testcase.get('FormattedID'),
_ref: testcase.get('_ref'),
Name: testcase.get('Name'),
ResultsCount: testcase.get('Results').Count,
Results: []
};
var results = testcase.getCollection('Results');
results.load({
fetch: ['Verdict','Date','Build'],
callback: function(records, operation, success){
Ext.Array.each(records, function(result){
tc.Results.push({_ref: result.get('_ref'),
Verdict: result.get('Verdict'),
Date: result.get('Date'),
Build: result.get('Build'),
});
},this);
--pendingResults;
if (pendingResults === 0) {
this.updateGrid2(testCases);
}
},
scope:this
});
testCases.push(tc);
}, this);
},
updateGrid2: function(testCases){
console.log(testCases);
var store = Ext.create('Rally.data.custom.Store', {
data: testCases,
pageSize: 100
});
if (!this.down('#g2')) {
this.createGrid2(store);
}
else{
this.down('#g2').reconfigure(store);
}
},
createGrid2: function(store){
console.log("load grid", store);
var that = this;
var g2 = Ext.create('Rally.ui.grid.Grid', {
id: 'g2',
store: store,
columnCfgs: [
{
text: 'Formatted ID', dataIndex: 'FormattedID', xtype: 'templatecolumn',
tpl: Ext.create('Rally.ui.renderer.template.FormattedIDTemplate')
},
{
text: 'Name', dataIndex: 'Name',
},
{
text: 'Results Count', dataIndex: 'ResultsCount',
},
{
text: 'Results', dataIndex: 'Results', flex:1,
renderer: function(value) {
var html = [];
Ext.Array.each(value, function(result){
html.push('<b>Verdict:</b> ' + result.Verdict + '<br />' + '<b>Date:</b> ' + Rally.util.DateTime.toIsoString(result.Date,true) + '<br />' + '<b>Build</b> ' + result.Build + '<br />')
});
return html.join('<br /><br />');
}
}
]
});
this.down('#childPanel2').add(g2);
}
});

How do I Query For Defects where with a requirement under a particular feature

When using a custom grid query based on defects I often filter based on the tags of the linked user story. I would like to use the actual feature hierarchy. e.g. Show all defects where the linked story is under a given feature or initiative. I could not work this out from looking at the documentation
Tags attribute exists on Artifact object from which Requirement inherits, hence Requirement.Tags can be traversed. Feature attribute does not exist on Requirement. It exists on HierarchicalRequirement, which inherits from Requirement, hence Requirement.Feature cannot be traversed.
In this context a custom grid may not be a suitable choice. But a custom app can be written that shows all those relationships. Here is a custom app that has two comboboxes: a Release and a Feature. The Feature combobox is populated based on the selection in the Release combobox. When a feature is selected from the second combobox a grid is populated with the feature's child stories and defects (if any) associated with those stories.
You may see the full code in this repo, and copy the html file into a custom page.
Here is the js file:
Ext.define('CustomApp', {
extend: 'Rally.app.TimeboxScopedApp',
componentCls: 'app',
scopeType: 'release',
comboboxConfig: {
fieldLabel: 'Select a Release:',
labelWidth: 100,
width: 300
},
addContent: function() {
this._makeCombobox();
},
onScopeChange: function() {
this._makeCombobox();
},
_makeCombobox: function() {
if (this.down('#features')) {
this.down('#features').destroy();
}
var features = Ext.create('Rally.ui.combobox.ComboBox',{
id: 'features',
storeConfig: {
model: 'PortfolioItem/Feature',
fetch: ['FormattedID','Name','Release', 'UserStories'],
pageSize: 100,
autoLoad: true,
filters: [this.getContext().getTimeboxScope().getQueryFilter()]
},
fieldLabel: 'select Feature',
listeners:{
ready: function(combobox){
if (combobox.getRecord()) {
console.log('ready',combobox.getRecord().get('_ref'));
this._onFeatureSelected(combobox.getRecord());
}
else{
console.log('selected release has no features');
if (this.down('#grid')) {
this.down('#grid').destroy();
}
}
},
select: function(combobox){
if (combobox.getRecord()) {
console.log('select',combobox.getRecord().get('_ref'));
this._onFeatureSelected(combobox.getRecord());
}
},
scope: this
}
});
this.add(features);
},
_onFeatureSelected:function(feature){
console.log('feature', feature.get('Name'));
var f = {
FormattedID: feature.get('FormattedID'),
Name: feature.get('Name'),
_ref: feature.get("_ref"),
UserStories: []
};
var collection = feature.getCollection('UserStories', {fetch: ['Name','FormattedID','Owner', 'Defects']});
var that = this;
var count = collection.getCount();
console.log(count);
var stories = [];
var pendingStories = count;
collection.load({
callback: function(records, operation, success){
Ext.Array.each(records, function(story){
var s = {
FormattedID: story.get('FormattedID'),
Name: story.get('Name'),
_ref: story.get("_ref"),
DefectCount: story.get('Defects').Count,
Defects: []
};
var defects = story.getCollection('Defects');
var defectcount = defects.getCount();
var pendingDefects = defectcount;
defects.load({
fetch: ['FormattedID'],
callback: function(records, operation, success){
Ext.Array.each(records, function(defect){
s.Defects.push({_ref: defect.get('_ref'),
FormattedID: defect.get('FormattedID')
});
}, this);
--pendingDefects;
if (pendingDefects === 0) {
console.log(story.get('FormattedID') + ' - ' + story.get('Name'));
--pendingStories;
if (pendingStories === 0) {
console.log('stories inside callback',stories);
}
}
console.log('makeGrid');
that._makeGrid(stories);
},
scope: this
});
stories.push(s);
}, this);
}
});
},
_makeGrid: function(stories) {
var c = Ext.create('Ext.Container', {
layout: {
type: 'absolute'
},
x: 400
});
this.add(c);
this._store = Ext.create('Rally.data.custom.Store', {
data: stories,
pageSize: 100,
remoteSort:false
});
if (!this.down('#grid')){
c.add({
xtype: 'rallygrid',
itemId: 'grid',
store: this._store,
columnCfgs: [
{
text: 'Formatted ID', dataIndex: 'FormattedID', xtype: 'templatecolumn',
tpl: Ext.create('Rally.ui.renderer.template.FormattedIDTemplate')
},
{
text: 'Name', dataIndex: 'Name'
},
{
text: 'Defect Count', dataIndex: 'DefectCount'
},
{
text: 'Defects', dataIndex: 'Defects',
renderer: function(value) {
var html = [];
Ext.Array.each(value, function(defect){
html.push('' + defect.FormattedID + '')
});
return html.join(', ');
}
}
]
});
}
else{
this.down('#grid').reconfigure(this._store);
}
}
});

How do I obtain the User object from User Story reference in Rally Web Services API?

I queried for a WSAPI DataStore of PortfolioItem/Feature. With each Feature object, I call getCollection('UserStories') to get the array of user stories that Feature. For each User Story I would like to grab the Owner information.
How do I get the actual User object from my reference to the Owner field that is available on the User Story object?
In addition to WsapiDataStore, a Rally.data.custom.Store is needed, where the owner is accessed like this:
Ext.Array.each(records, function(story){
f.UserStories.push({_ref: story.get('_ref'),
FormattedID: story.get('FormattedID'),
Owner: (story.get('Owner') && story.get('Owner')._refObjectName) || 'None'
});
}
Here is a code that builds a grid of Features with their children stories, and the Owners of the stories.
Ext.define('CustomApp', {
extend: 'Rally.app.App',
componentCls: 'app',
launch: function() {
Ext.create('Rally.data.WsapiDataStore', {
model: 'PortfolioItem/Feature',
fetch: ['FormattedID','Name','UserStories'],
pageSize: 100,
autoLoad: true,
listeners: {
load: this._onDataLoaded,
scope: this
}
});
},
_createGrid: function(features) {
this.add({
xtype: 'rallygrid',
store: Ext.create('Rally.data.custom.Store', {
data: features,
pageSize: 100
}),
columnCfgs: [
{
text: 'Formatted ID', dataIndex: 'FormattedID', xtype: 'templatecolumn',
tpl: Ext.create('Rally.ui.renderer.template.FormattedIDTemplate')
},
{
text: 'Name', dataIndex: 'Name'
},
{
text: 'Story Count', dataIndex: 'StoryCount'
},
{
text: 'User Stories', dataIndex: 'UserStories',
renderer: function(value) {
var html = [];
Ext.Array.each(value, function(userstory){
html.push('' + userstory.FormattedID + '')
});
return html.join(', ');
}
},
{
text: 'Owner', dataIndex: 'UserStories',
renderer: function(value) {
var html = [];
Ext.Array.each(value, function(userstory){
html.push(userstory.Owner);
});
return html.join(', ');
}
}
]
});
},
_onDataLoaded: function(store, data){
var features = [];
var pendingstories = data.length;
var owner;
Ext.Array.each(data, function(feature) {
var f = {
FormattedID: feature.get('FormattedID'),
Name: feature.get('Name'),
_ref: feature.get("_ref"),
StoryCount: feature.get('UserStories').Count,
UserStories: []
};
var stories = feature.getCollection('UserStories');
stories.load({
fetch: ['FormattedID','Owner'],
callback: function(records, operation, success){
Ext.Array.each(records, function(story){
f.UserStories.push({_ref: story.get('_ref'),
FormattedID: story.get('FormattedID'),
Owner: (story.get('Owner') && story.get('Owner')._refObjectName) || 'None'
});
}, this);
--pendingstories;
if (pendingstories === 0) {
this._createGrid(features);
}
},
scope: this
});
features.push(f);
}, this);
}
});