I'm writing an application that should load Accepted stories that have tasks with integer in the Time Spent field.
As I can see in some tests I made the task fields that are accessible through UserStory is: 'Tasks', 'TaskActualTotal', 'TaskEstimateTotal', 'TaskRemainingTotal' and 'TaskStatus'.
Tasks has a _ref attribute with a link to a JSON with all tasks for this story. How may I explore this since I'm using Rally API? Or Is there a better way to do this?
UPDATE:
So this is pretty much i have now.
var storiesCall = Ext.create('Rally.data.WsapiDataStore', {
model: 'UserStory',
fetch: ['Tasks']
});
storiesCall.load().then({
success: this.loadTasks,
scope: this
});
loadTasks: function(stories) {
storiesCall = _.flatten(stories);
console.log(stories.length);
_.each(stories, function(story) {
var tasks = story.get('Tasks');
if(tasks.Count > 0) {
tasks.store = story.getCollection('Tasks', {fetch: ['Name','FormattedID','Workproduct','Estimate','TimeSpent']});
console.log(tasks.store.load().deferred);
}
else{
console.log('no tasks');
}
});
}
tasks.store.load().deferred is returning the following object:
Note that we can see the task data on value[0] but when i try to wrap it out with tasks.store.load().deferred.value[0] its crashing.
Any thoughts?
Per WS API doc, TimeSpent field on Task object (which is populated automatically from entries in Rally Timesheet/Time Tracker module) cannot be used in queries, so something like this (TimeSpent > 0) will not work.
Also, a UserStory object (referred to as HierarchicalRequirement in WS API) does not have a field where child tasks' TimeSpent rolls up to the story similar to how child tasks' Estimate rolls up to TaskEstimateTotal on a story.
It is possible to get TimeSpent for each task and then add them up by accessing a story's Tasks collection as done in this app:
Ext.define('CustomApp', {
extend: 'Rally.app.App',
componentCls: 'app',
launch: function() {
var initiatives = Ext.create('Rally.data.wsapi.Store', {
model: 'PortfolioItem/Initiative',
fetch: ['Children']
});
initiatives.load().then({
success: this.loadFeatures,
scope: this
}).then({
success: this.loadParentStories,
scope: this
}).then({
success: this.loadChildStories,
scope: this
}).then({
success: this.loadTasks,
failure: this.onFailure,
scope: this
}).then({
success: function(results) {
results = _.flatten(results);
_.each(results, function(result){
console.log(result.data.FormattedID, 'Estimate: ', result.data.Estimate, 'WorkProduct:', result.data.WorkProduct.FormattedID, 'TimeSpent', result.data.TimeSpent );
});
this.makeGrid(results);
},
failure: function(error) {
console.log('oh, noes!');
},
scope: this
});
},
loadFeatures: function(initiatives) {
var promises = [];
_.each(initiatives, function(initiative) {
var features = initiative.get('Children');
if(features.Count > 0) {
features.store = initiative.getCollection('Children',{fetch: ['Name','FormattedID','UserStories']});
promises.push(features.store.load());
}
});
return Deft.Promise.all(promises);
},
loadParentStories: function(features) {
features = _.flatten(features);
var promises = [];
_.each(features, function(feature) {
var stories = feature.get('UserStories');
if(stories.Count > 0) {
stories.store = feature.getCollection('UserStories', {fetch: ['Name','FormattedID','Children']});
promises.push(stories.store.load());
}
});
return Deft.Promise.all(promises);
},
loadChildStories: function(parentStories) {
parentStories = _.flatten(parentStories);
var promises = [];
_.each(parentStories, function(parentStory) {
var children = parentStory.get('Children');
if(children.Count > 0) {
children.store = parentStory.getCollection('Children', {fetch: ['Name','FormattedID','Tasks']});
promises.push(children.store.load());
}
});
return Deft.Promise.all(promises);
},
loadTasks: function(stories) {
stories = _.flatten(stories);
var promises = [];
_.each(stories, function(story) {
var tasks = story.get('Tasks');
if(tasks.Count > 0) {
tasks.store = story.getCollection('Tasks', {fetch: ['Name','FormattedID','Workproduct','Estimate','TimeSpent']});
promises.push(tasks.store.load());
}
else{
console.log('no tasks');
}
});
return Deft.Promise.all(promises);
},
makeGrid:function(tasks){
var data = [];
_.each(tasks, function(task){
data.push(task.data);
});
_.each(data, function(record){
record.Story = record.WorkProduct.FormattedID + " " + record.WorkProduct.Name;
});
this.add({
xtype: 'rallygrid',
showPagingToolbar: true,
showRowActionsColumn: true,
editable: false,
store: Ext.create('Rally.data.custom.Store', {
data: data,
groupField: 'Story'
}),
features: [{ftype:'groupingsummary'}],
columnCfgs: [
{
xtype: 'templatecolumn',text: 'ID',dataIndex: 'FormattedID',width: 100,
tpl: Ext.create('Rally.ui.renderer.template.FormattedIDTemplate'),
summaryRenderer: function() {
return "Totals";
}
},
{
text: 'Name',dataIndex: 'Name'
},
{
text: 'TimeSpent',dataIndex: 'TimeSpent',
summaryType: 'sum'
},
{
text: 'Estimate',dataIndex: 'Estimate',
summaryType: 'sum'
}
]
});
}
});
Related
I have an app route where I want to be able to use query parameters for my where clauses if there is a query present. My initial approach was to use an if/else clause in the get and return two different queries depending on if the query parameters were present, but I get a SyntaxError: Unexpected token . error at my then(function..., which is telling me that this isn't the right approach. How can I achieve something with Sequelize?
/*==== / ====*/
appRoutes.route('/')
.get(function(req, res){
console.log(req.query.dataDateStart);
console.log(req.query.dataDateEnd);
if(req.query.dataDateStart && req.query.dataDateEnd){
return models.Comment.findAll({
where: {
dataDateStart: {
$gte: dateFormatting(req.body.dataDateStart)
},
dataDateEnd: {
$lte: dateFormatting(req.body.dataDateEnd)
}
},
order: 'commentDate DESC',
include: [{
model: models.User,
where: { organizationId: req.user.organizationId },
attributes: ['organizationId', 'userId']
}],
limit: 10
})
} else {
return models.Comment.findAll({
order: 'commentDate DESC',
include: [{
model: models.User,
where: { organizationId: req.user.organizationId },
attributes: ['organizationId', 'userId']
}],
limit: 10
})
}
.then(function(comment){
function feedLength(count){
if (count >= 10){
return 2;
} else {
return null;
}
};
res.render('pages/app/activity-feed.hbs',{
comment: comment,
user: req.user,
secondPage: feedLength(comment.length)
});
});
})
.post(function(req, res){
function dateFormatting(date){
var newDate = new Date(date);
return moment.utc(newDate).format();
}
console.log("This is a date test" + dateFormatting(req.body.dataDateStart));
//Testing if the query will come through correctly.
models.Comment.findAll({
order: 'commentDate DESC',
where: {
dataDateStart: {
$gte: dateFormatting(req.body.dataDateStart)
},
dataDateEnd: {
$lte: dateFormatting(req.body.dataDateEnd)
}
},
include: [{
model: models.User,
where: {
organizationId: req.user.organizationId,
},
attributes: ['organizationId', 'userId']
}],
limit: 10
}).then(function(filterValues) {
var dataDateStart = encodeURIComponent(dateFormatting(req.body.dataDateStart));
var dataDateEnd = encodeURIComponent(dateFormatting(req.body.dataDateEnd));
res.redirect('/app?' + dataDateStart + '&' + dataDateEnd);
}).catch(function(error){
res.send(error);
})
});
This is a syntax error. The then function can only be called on a thenable object. In the code snipped above, .then is applied to nothing. Instead, it is called after an if-else statement.
if(...) {
...
}
else {
...
}
// .then() is not called on any object --> syntax error 'unexpected "."'
.then()
If you just want to configure the where parameters, you could define the where object depending on the url queries.
appRoutes.route('/')
.get(function(req, res){
console.log(req.query.dataDateStart);
console.log(req.query.dataDateEnd);
var whereObject = {};
// CHeck for queries in url
if(req.query.dataDateStart && req.query.dataDateEnd){
whereObject = {
dataDateStart: {
$gte: dateFormatting(req.body.dataDateStart)
},
dataDateEnd: {
$lte: dateFormatting(req.body.dataDateEnd)
}
};
}
models.Comment.findAll({
where: whereObject,
order: 'commentDate DESC',
include: [{
model: models.User,
where: { organizationId: req.user.organizationId },
attributes: ['organizationId', 'userId']
}],
limit: 10
})
.then(function(comment){
function feedLength(count){
if (count >= 10){
return 2;
} else {
return null;
}
};
res.render('pages/app/activity-feed.hbs',{
comment: comment,
user: req.user,
secondPage: feedLength(comment.length)
});
});
})
.post(function(req, res){
function dateFormatting(date){
var newDate = new Date(date);
return moment.utc(newDate).format();
}
console.log("This is a date test" + dateFormatting(req.body.dataDateStart));
//Testing if the query will come through correctly.
models.Comment.findAll({
order: 'commentDate DESC',
where: {
dataDateStart: {
$gte: dateFormatting(req.body.dataDateStart)
},
dataDateEnd: {
$lte: dateFormatting(req.body.dataDateEnd)
}
},
include: [{
model: models.User,
where: {
organizationId: req.user.organizationId,
},
attributes: ['organizationId', 'userId']
}],
limit: 10
}).then(function(filterValues) {
var dataDateStart = encodeURIComponent(dateFormatting(req.body.dataDateStart));
var dataDateEnd = encodeURIComponent(dateFormatting(req.body.dataDateEnd));
res.redirect('/app?' + dataDateStart + '&' + dataDateEnd);
}).catch(function(error){
res.send(error);
})
});
We want to take a number test cases from previous iterations / test sets and import them into a new test set. The problem is that we cannot export the test cases from a test set into CSV or any other data format as all we get is a printable report. We have also tried copy and paste the printable report into MS Excel but it does not give a data format.
Any suggestions / HTML forms that can be used.
It is possible to copy TestSets in Rally UI and that functionality allows re-using member Test Cases from Iteration to Iteration or from Release to Release. When you copy Test Sets they preserve the member Test Cases, including Test Steps. Rally recommends this method to handle testing. If the same tests are use repeatedly, copy the test sets that contain them into the new iteration directly in the Rally UI. This resets the results for that iteration and keeps you from duplicating the testcases.
Mark's Ruby tools provide an alternative which allows selective copying of member test cases - something that does not happen when a testset is copied in the UI.
Here is a javascript example using AppSDK2. You may certainly customize it further. The main point of this example is to illustrate how to update a collection using AppSDK2.
A user may select a "source" iteration from the iteration combobox, then a testset combobox is populated with testsets scheduled for this iteration. Next a user can select a destination iteration from the third combobox that limits iterations to current and future iterations, and create a new testset scheduled for the "destination" iteration. The testcases from the "source" testset are copied to the new testset.
Currently updating collections will not work if the app is run outside of Rally, but this limitation will be corrected soon. It will allow collection updates outside of rally using CORS as long as rab run command is used. The code below should work inside Rally now. The deployment html is available from this github repo.
Ext.define('CustomApp', {
extend: 'Rally.app.TimeboxScopedApp',
componentCls: 'app',
scopeType: 'iteration',
comboboxConfig: {
fieldLabel: 'Select a source Iteration',
labelWidth: 150,
width: 350
},
onScopeChange: function() {
if (!this.down('#parentPanel')) {
this._panel = Ext.create('Ext.panel.Panel', {
layout: 'hbox',
itemId: 'parentPanel',
componentCls: 'panel',
items: [
{
xtype: 'container',
itemId: 'pickerContainer',
},
{
xtype: 'container',
itemId: 'iterationContainer',
}
]
});
this.add(this._panel);
}
if (this.down('#testSetComboxBox')) {
this.down('#testSetComboxBox').destroy();
}
var testSetComboxBox = Ext.create('Rally.ui.combobox.ComboBox',{
itemId: 'testSetComboxBox',
storeConfig: {
model: 'TestSet',
limit: Infinity,
pageSize: 100,
autoLoad: true,
filters: [this.getContext().getTimeboxScope().getQueryFilter()]
},
fieldLabel: 'Select a TestSet',
listeners:{
ready: function(combobox){
if (combobox.getRecord()) {
this._onTestSetSelected(combobox.getRecord());
}
else{
console.log('selected iteration has no testsets');
}
},
select: function(combobox){
if (combobox.getRecord()) {
this._onTestSetSelected(combobox.getRecord());
}
},
scope: this
}
});
this.down('#pickerContainer').add(testSetComboxBox);
},
_onTestSetSelected:function(testset){
var id = testset.get('ObjectID');
this._name = testset.get('Name');
testset.self.load(id, {
fetch: ['Name','TestCases'],
callback: this._onSourceRecordRead,
scope: this
});
},
_onSourceRecordRead: function(record) {
var that = this;
that._testcases = [];
var testcaseStore = record.getCollection('TestCases',{fetch:['Name','FormattedID']});
testcaseStore.load({
callback: function() {
_.each(testcaseStore.getRange(), function(tc){
that._testcases.push(tc.data._ref);
});
console.log(that._testcases);
that._selectFutureIteration();
}
});
},
_selectFutureIteration: function(){
if (!this.down('#iterationComboxBox')) {
var iterationComboxBox = Ext.create('Rally.ui.combobox.ComboBox',{
itemId: 'iterationComboxBox',
storeConfig: {
model: 'Iteration',
limit: Infinity,
pageSize: 100,
autoLoad: true,
filters: [
{
property: 'StartDate',
operator: '>=',
value: (new Date()).toISOString()
}
]
},
fieldLabel: 'Select a destination Iteration',
labelWidth: 150,
listeners:{
ready: function(combobox){
if (combobox.getRecord()) {
this._onFutureIterationSelected(combobox.getRecord());
}
else{
console.log('no current or future iterations');
}
},
select: function(combobox){
if (combobox.getRecord()) {
this._onFutureIterationSelected(combobox.getRecord());
}
},
scope: this
}
});
this.down('#iterationContainer').add(iterationComboxBox);
}
},
_onFutureIterationSelected:function(iteration){
var that = this;
that._iteration = iteration.data._ref;
if (!this.down('#create')) {
var createButton = Ext.create('Ext.Container', {
items: [
{
xtype : 'rallybutton',
text : 'create a testset',
itemId: 'create',
handler: function() {
that._createTestSet();
}
}
]
});
this.add(createButton);
}
},
_createTestSet: function(){
var that = this;
console.log('create testset scheduled for ', that._iteration);
Rally.data.ModelFactory.getModel({
type: 'TestSet',
success: function(model) {
that._model = model;
var ts = Ext.create(model, {
Name: that._name + 'Copy',
Iteration: that._iteration
});
ts.save({
callback: function(result, operation) {
if(operation.wasSuccessful()) {
console.log(result.get('Name'), ' ', result.get('Iteration')._refObjectName);
that._readRecord(result);
}
else{
console.log("?");
}
}
});
}
});
},
_readRecord: function(result) {
var id = result.get('ObjectID');
this._model.load(id, {
fetch: ['Name','TestCases'],
callback: this._onRecordRead(result),
scope: this
});
},
_onRecordRead: function(record, operation) {
console.log('There are ', record.get('TestCases').Count, ' in ', record.get('Name') );
var that = this;
var testcaseStore = record.getCollection('TestCases');
testcaseStore.load({
callback: function() {
testcaseStore.add(that._testcases);
testcaseStore.sync({
callback: function() {
console.log('success');
}
});
}
});
}
});
There are a couple of Ruby scripts that might help you here:
https://github.com/markwilliams970/Rally-Test-Set-Export
https://github.com/markwilliams970/Rally-Add-TestCases-TestSet
These do require a working Ruby scripting environment and some familiarity with configuring/running scripts at the command line. However, working in combination, these should be enough to accomplish your goal.
I have an app that I am trying to use but it seems that while iterating through arrays and pushing into another array.i.e combining the arrays into one is not working for me. Example - I see all 213 pushes to this array but when I check its contents they are less.
Here is the code that shows me incomplete array push list.
For 213 test cases test set only 67 are pushed and present in the array
that = this;
that._testSetTestList = [];
console.log('testsetdata',testsetdata);
Ext.Array.each(testsetdata, function(record) {
console.log('record.get(TestCases)',record.get('TestCases'));
Ext.Array.each(record.get('TestCases'), function(name, index) {
that._testSetTestList.push({
resID: name.FormattedID,
resName: name.Name,
resObject: name.ObjectID,
resSetID: record.get('FormattedID'),
resSet: record.get('Name'),
resSetObject: record.get('ObjectID'),
resSetProject: name.Project.ObjectID
});
console.log('_testSetTestList.push',{
resID: name.FormattedID
});
});
});
Can anyone guide me to what I am doing wrong if anything.
Try using this code instead:
this._testSetTestList = Ext.Array.flatten(Ext.Array.map(testsetdata, function(record) {
return Ext.Array.map(record.get('TestCases'), function(name, index) {
return {
resID: name.FormattedID,
resName: name.Name,
resObject: name.ObjectID,
resSetID: record.get('FormattedID'),
resSet: record.get('Name'),
resSetObject: record.get('ObjectID'),
resSetProject: name.Project.ObjectID
};
});
}))
The issue in my case was not the code but the scope..I was trying to get the test case results for test cases that were not directly reachable in the project tree. We have the test cases residing in several projects but then we use them in test sets under different projects. If the test cases that are part of the queried test sets are directly reachable for the project for which we view this page, then test case results were accounted for BUT if the test cases were in projects that were siblings to the one that view the app from then the query could not find them and take their test case results. The solution was to consolidate all test cases under the correct project so that the app can access them from any required project.
Based on the example of using promises from this github repo, here is a code that builds a grid of test sets with their collection of test cases, where elements of array of test sets setsWithCases is pushed in to another array testsets, and the second array is used to populate the grid. The second array contains all elements of the first array. I am using LowDash _.each, included with AppSDK2.
Ext.define('CustomApp', {
extend: 'Rally.app.App',
componentsCls: 'app',
launch: function(){
var aStore = Ext.create('Rally.data.wsapi.Store', {
model: 'TestSet',
fetch: ['ObjectID', 'FormattedID', 'Name', 'TestCases'],
autoLoad: true,
context:{
projectScopeUp: false,
projectScopeDown: false
},
listeners:{
scope: this,
load: this._onStoreLoaded
}
});
},
_onStoreLoaded: function(store, records){
var setsWithCases = [];
var testsets = [];
var that = this;
var promises = [];
_.each(records, function(testset){
promises.push(that._getTestCases(testset, that));
});
Deft.Promise.all(promises).then({
success: function(results) {
_.each(results, function(result) {
if (result.TestCases.length > 0) {
setsWithCases.push(result);
}
});
_.each(setsWithCases, function(testset){
testsets.push(testset);
});
that._makeGrid(testsets);
}
});
},
_getTestCases:function(testset, scope){
var deferred = Ext.create('Deft.Deferred');
var that = scope;
var testcases = [];
var result = {};
var testsetRef = testset.get('_ref');
var testsetObjectID = testset.get('ObjectID');
var testsetFormattedID = testset.get('FormattedID');
var testsetName = testset.get('Name');
var testcaseCollection = testset.getCollection("TestCases", {fetch: ['Name', 'FormattedID']});
var testcaseCount = testcaseCollection.getCount();
testcaseCollection.load({
callback: function(records, operation, success){
_.each(records, function(testcase){
testcases.push(testcase);
});
result = {
"_ref": testsetRef,
"ObjectID": testsetObjectID,
"FormattedID": testsetFormattedID,
"Name": testsetName,
"TestCases": testcases
};
deferred.resolve(result);
}
});
return deferred;
},
_makeGrid:function(testsets){
var that = this;
var gridStore = Ext.create('Rally.data.custom.Store', {
data: testsets,
pageSize: 1000,
remoteSort: false
});
var aGrid = Ext.create('Rally.ui.grid.Grid', {
itemId: 'testsetGrid',
store: gridStore,
columnCfgs:[
{
text: 'Formatted ID', dataIndex: 'FormattedID', xtype: 'templatecolumn',
tpl: Ext.create('Rally.ui.renderer.template.FormattedIDTemplate')
},
{
text: 'Name', dataIndex: 'Name', flex: 1
},
{
text: 'TestCases', dataIndex: 'TestCases', flex:1,
renderer: function(value) {
var html = [];
_.each(value, function(testcase){
html.push('' + testcase.get('FormattedID') + '' + ' ' + testcase.get('Name'));
});
return html.join(', ');
}
}
]
});
that.add(aGrid);
}
});
I am trying to create a simple rallycardboard app that displays projects as columns with the project backlog stories as cards. Then allow the drag/drop of cards to set the project. Code is attached.
If I specify 'Project" as the attribute, the board contains columns for all projects in the workspace. I wish to limit the columns shown to either
Scoped parent and children, or
Code a list of project columns. I have tried the manipulate the columns, columnConfig, context settings, but nothing produces the desired results.
<!DOCTYPE html>
<html>
<head>
<title>CardBoard Example</title>
<script type="text/javascript" src="/apps/2.0rc2/sdk.js"></script>
<script type="text/javascript">
Rally.onReady(function() {
Ext.define('ProjBoard', {
extend: 'Rally.app.App',
launch: function() {
if (cardBoardConfig) {
cardBoardConfig.destroy();
}
var cardBoardConfig = {
xtype: 'rallycardboard',
types: ['User Story'],
attribute: 'Project',
fieldToDisplay: 'Project',
cardConfig: {
fields: ['Project', 'Parent','Iteration']
},
storeConfig: {
filters: [
{ property: 'ScheduleState', operator: '<', value: 'In-Progress' },
{ property: 'Iteration', operator: '=', value: '' }
],
sorters: [
{ property: 'Rank', direction: 'DESC' }
],
//Specify current project and scoping
context: this.getContext().getDataContext()
}
};
this.add(cardBoardConfig);
}
});
Rally.launchApp('ProjBoard', {
name: 'Backlog Project Board'
});
});
</script>
<style type="text/css">
</style>
</head>
<body></body>
</html>
You should be able to specify the columns via config:
https://help.rallydev.com/apps/2.0rc2/doc/#!/api/Rally.ui.cardboard.CardBoard-cfg-columns
columns: [
{
value: '/project/12345',
columnHeaderConfig: {
headerTpl: '{project}',
headerData: {project: 'Project 1'}
}
},
//more columns...
]
The code below allowed me to cut down a dozen of project columns to three. First I get current project and query a collection of its child projects to build an array of projects I want to have on the board (you may choose a different criteria for what projects you want on the board), and then I extended Rally.ui.cardboard.CardBoard to overwrite its _buildColumnsFromModel method where only columns that meet this condition are filtered in :
retrievedColumns = _.select(retrievedColumns, function(project){
return that.arrayOfProjectRefs.indexOf(project.value) != -1
});
Here is the full js file. Apart from those changes, this is your code.
Ext.define('CustomApp', { extend: 'Rally.app.App', componentCls: 'app',
launch: function() {
var that = this;
that.arrayOfProjectRefs = [];
var p = this.getContext().getProject();
Ext.create('Rally.data.wsapi.Store', {
model: 'Project',
fetch: ['Children'],
filters:[
{
Property: '_ref',
value: p
}
],
pageSize: 1,
autoLoad: true,
listeners: {
load: function(store, records) {
var project = records[0];
var childProjects = project.get('Children');
var childProjectsCount = project.get('Children').Count;
console.log('childProjectsCount', childProjectsCount);
that.arrayOfProjectRefs.push(project.get('_ref'));
project.getCollection('Children').load({
fetch: ['_ref', 'Name', 'State'],
callback: function(records, operation, success) {
Ext.Array.each(records, function(child) {
console.log(child.get('_ref') + ' - ' + child.get('Name') + child.get('State'));
if (child.get('State') === 'Open') {
that.arrayOfProjectRefs.push(child.get('_ref'));
--childProjectsCount;
if (childProjectsCount === 0) {
that._buildBoard();
}
}
});
}
});
}
}
});
},
_buildBoard:function(){
var that = this;
console.log('app._arrayOfProjectRefs', this.arrayOfProjectRefs);
Ext.define('ProjectCardboard', {extend: 'Rally.ui.cardboard.CardBoard',
xtype: 'projectCardboard',
_buildColumnsFromModel: function() {
var model = this.models[0];
if (model) {
var attribute = model.getField('Project');
if (attribute) {
attribute.getAllowedValueStore().load({
callback: function(records, operation, success) {
var retrievedColumns = _.map(records, function(allowedValue) {
var displayValue, value = allowedValue.get('StringValue');
if (!value && attribute.attributeDefinition.AttributeType.toLowerCase() === 'rating') {
value = "None";
} else if (attribute.attributeDefinition.AttributeType.toLowerCase() === 'object') {
displayValue = value;
value = allowedValue.get('_ref');
if (value === 'null') {
value = null;
}
}
return {
value: value,
columnHeaderConfig: {
headerTpl: displayValue || value || 'None'
}
};
});
this.fireEvent('columnsretrieved', this, retrievedColumns);
retrievedColumns = _.select(retrievedColumns, function(project){
return that.arrayOfProjectRefs.indexOf(project.value) != -1
});
console.log('retrievedColumns after filter', retrievedColumns)
this.columnDefinitions = [];
_.each(retrievedColumns, this.addColumn, this);
this.renderColumns();
},
scope: this
});
}
}
}
});
var addNewConfig = {
xtype: 'rallyaddnew',
recordTypes: ['User Story'],
ignoredRequiredFields: ['Name', 'Iteration'],
showAddWithDetails: false,
};
this.addNew = this.add(addNewConfig);
var myCardConfig = {
xtype: 'rallycard',
fields: ['ScheduleState','Name'],
maxHeight: 100
}
var cardBoardConfig = {
xtype: 'projectCardboard',
types: ['User Story'],
attribute: 'Project',
cardConfig: myCardConfig
};
this.cardBoard = this.add(cardBoardConfig);
}
});
I'm fairly new to Rally and so far have only used the web interface (I haven't used the Rally APIs from a programming languages yet). Occasionally we have a test set that we don't finish in an iteration, so we'd like to be able to copy the test set to the next iteration but retain the test case results entered so far in the new iteration so that we don't have to look in 2 different places for the complete test set results. Perhaps one solution is better iteration planning, but I'm still curious if there's a way to copy test case results along with a test set when copying a test set.
Test Case Result cannot be copied. It can be created in UI or Web Services API with the identical data, but it does not support a copy functionality.
For a general example of how to create a TCR object using a browser REST client see this SO post here.
Here is a poof of concept app that creates a copy of an existing TCR. The app filters TestSets by Iteration, and loads those TestSets into a combobox. Based on the TestSet selection in the combobox another combobox is created, populated with TestCases. When a Test Case is selected from that combobox a grid is built with TestCaseResults. Doubleclicking on a TCR in the grid will invoke a copy function. See AppSDK2 topic on how to copy records here. You may extend the code per your specifications. The source is in this github repo.
Minimally you need to change ObjectIDs of destination test case and destination test set in _copyRecordOnDoubleClick method below:
Ext.define('CustomApp', {
extend: 'Rally.app.TimeboxScopedApp',
componentCls: 'app',
scopeType: 'iteration',
comboboxConfig: {
fieldLabel: 'Select an Iteration:',
labelWidth: 100,
width: 300
},
onScopeChange: function() {
if (this.down('#testSetComboxBox')) {
this.down('#testSetComboxBox').destroy();
}
if (this.down('#testCaseComboxBox')) {
this.down('#testCaseComboxBox').destroy();
}
if (this.down('#resultsGrid')) {
this.down('#resultsGrid').destroy();
}
var testSetComboxBox = Ext.create('Rally.ui.combobox.ComboBox',{
id: 'testSetComboxBox',
storeConfig: {
model: 'TestSet',
pageSize: 100,
autoLoad: true,
filters: [this.getContext().getTimeboxScope().getQueryFilter()]
},
fieldLabel: 'select TestSet',
listeners:{
ready: function(combobox){
if (combobox.getRecord()) {
this._onTestSetSelected(combobox.getRecord());
}
else{
console.log('selected iteration has no testsets');
}
},
select: function(combobox){
if (combobox.getRecord()) {
this._onTestSetSelected(combobox.getRecord());
}
},
scope: this
}
});
this.add(testSetComboxBox);
},
_onTestSetSelected:function(selectedTestset){
var testCases = selectedTestset.getCollection('TestCases', {fetch: ['FormattedID','ObjectID', 'Results']});
var ts = {
FormattedID: selectedTestset.get('FormattedID'),
TestCaseCount: selectedTestset.get('TestCases').Count,
TestCases: [],
ResultCount: 0
};
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;
ts.TestCases.push({_ref: testcase.get('_ref'),
FormattedID: testcase.get('FormattedID'),
ObjectID: testcase.get('ObjectID')
});
}, this);
this._makeTestCaseCombobox(ts.TestCases);
},
scope: this
});
},
_makeTestCaseCombobox:function(testcases){
if (this.down('#testCaseComboxBox')) {
this.down('#testCaseComboxBox').destroy();
}
if (this.down('#resultsGrid')) {
this.down('#resultsGrid').destroy();
}
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
}
)
});
var filters = Ext.create('Rally.data.QueryFilter', filterArray[0]);
filterArray = _.rest(filterArray,1);
_.each(filterArray, function(filter){
filters = filters.or(filter)
},1);
var testCaseComboxBox = Ext.create('Rally.ui.combobox.ComboBox',{
id: 'testCaseComboxBox',
storeConfig: {
model: 'TestCase',
pageSize: 100,
autoLoad: true,
filters:filters,
fetch: true
},
fieldLabel: 'select TestCase',
listeners:{
ready: function(combobox){
if (combobox.getRecord()) {
this._onTestCaseSelected(combobox.getRecord());
}
else{
console.log('selected testset has no testcases');
}
},
select: function(combobox){
if (combobox.getRecord()) {
this._onTestCaseSelected(combobox.getRecord());
}
},
scope: this
}
});
this.add(testCaseComboxBox);
}
else{
console.log('selected testset has no testcases');
}
},
_onTestCaseSelected:function(selectedTestcase){
var results = selectedTestcase.getCollection('Results', {fetch: ['ObjectID','Date', 'TestSet', 'TestCase', 'Build', 'Verdict']});
var tc = {
ObjectID: selectedTestcase.get('ObjectID'),
FormattedID: selectedTestcase.get('FormattedID'),
Results: []
};
results.load({
callback: function(records, operation, success){
Ext.Array.each(records, function(result){
console.log("result.get('ObjectID')", result.get('ObjectID'));
console.log("result.get('Verdict')", result.get('Verdict'));
tc.Results.push({_ref: result.get('_ref'),
ObjectID: result.get('ObjectID'),
Date: result.get('Date'),
Build: result.get('Build'),
Verdict: result.get('Verdict')
});
}, this);
this._updateGrid(tc.Results);
},
scope: this
});
},
_updateGrid: function(results){
var store = Ext.create('Rally.data.custom.Store', {
data: results,
pageSize: 100
});
if (!this.down('#resultsGrid')) {
this._createGrid(store);
}
else{
this.down('#resultsGrid').reconfigure(store);
}
},
_createGrid: function(store){
var that = this;
var that = this;
var resultsGrid = Ext.create('Rally.ui.grid.Grid', {
id: 'resultsGrid',
store: store,
columnCfgs: [
{
text: 'ObjectID ID', dataIndex: 'ObjectID',
},
{
text: 'Date', dataIndex: 'Date',
},
{
text: 'Build', dataIndex: 'Build',
},
{
text: 'Verdict', dataIndex: 'Verdict',
},
],
listeners: {
celldblclick: function( grid, td, cellIndex, record, tr, rowIndex){
that._copyRecordOnDoubleClick(record);
}
}
});
this.add(resultsGrid);
},
_copyRecordOnDoubleClick: function(record){
var that = this;
console.log('record', record);
Rally.data.ModelFactory.getModel({
type: 'TestCaseResult',
success: function(model) {
that._model = model;
var copy = Ext.create(model, {
Date: record.get('Date'),
Build: record.get('Build'),
Verdict: record.get('Verdict'),
TestCase: '/testcase/17237838118',
TestSet: '/testset/17234968911'
});
copy.save({
callback: function(result, operation) {
if(operation.wasSuccessful()) {
console.log('result',result);
}
else{
console.log("problem");
}
}
});
}
});
}
});