With Rally SDK 2.0 APIs, I want to associate new TestCases to a given TestSet. To do this I :
initialize a store:
me.selectedTestCasesStore = myTestSet.getCollection('TestCases',{...});
Remove all items (I don't want to keep them):
me.selectedTestCasesStore.removeAll();
Add the new TestCases
me.selectedTestCasesStore.add({'_ref':aTestCaseRecord.data._ref});
Then synchronize
me.selectedTestCasesStore.sync({...});
Step 1 is OK : console.log(me.selectedTestCasesStore) shows me the collection in data.items[].
Step 2 seems OK as a console.log(me.selectedTestCasesStore) shows me nothing in data.items[] (previous records are gone).
Step 3 is OK because added test cases which were not present at step 1 are now in the collection
Step 4 : Called function is "success"
BUT... only new TestCases are added, the old ones are not removed, as if step 2 has no effect. What's wrong in my code ? I extract the part of the concerned code :
// me.selectedTestCasesStore : my store, with old TestCase associated to a TestSet.
// It is initialized with something like :
// me.selectedTestCasesStore = myTestSet.getCollection('TestCases',{...});
//
// selectedTestCasesArray : an array of records with the new TestCases to assign to the test set.
_removeAllFromSelectedTestCaseStore:function()
{
var me = this ;
console.log('In _removeAllFromSelectedTestCaseStore');
me.selectedTestCasesStore.addListener({
clear : me._addSelectedTestCasesToSelectedTestCaseStore,
scope : me,
});
// Remove all associated TestCases from selectedTestCases store
me.selectedTestCasesStore.removeAll();
},
_addSelectedTestCasesToSelectedTestCaseStore:function()
{
var me = this ;
console.log('In _addSelectedTestCasesToSelectedTestCaseStore');
console.log(' After remove, store is now :',me.selectedTestCasesStore);
// Add each check TestCase to selectedTestCases store
for (var i=0; i < me.selectedTestCasesArray.length; i++)
{
// Add it to the collection
me.selectedTestCasesStore.add({'_ref':me.selectedTestCasesArray[j].data._ref});
}
console.log(' After add, store is now :',me.selectedTestCasesStore);
// Synchronyze
me.selectedTestCasesStore.sync(
{
success: function(batch, options) {
//success!
console.log(' Success', me.selectedTestSetStore);
},
failure: function(batch, options){
console.log(' Faillure :(', me.selectedTestSetStore);
},
});
},
Thanks for your help !
This works for me instead of removeAll():
var testcases = testCaseStore.getRange();
_.each(testcases, function(testcase) {
testCaseStore.remove(testcase);
});
Here is the full js file that empties the test case collection on a test set before adding a new test case
Ext.define('CustomApp', {
extend: 'Rally.app.App',
componentCls: 'app',
launch: function() {
console.log("launch");
Rally.data.ModelFactory.getModel({
type: 'TestSet',
success: this._onModelRetrieved,
scope: this
});
},
_onModelRetrieved: function(model) {
console.log("_onModelRetrieved");
this.model = model;
this._readRecord(model);
},
_readRecord: function(model) {
var id = 16805375409;
console.log("_readRecord");
this.model.load(id, {
fetch: ['Name', 'FormattedID', 'TestCases'],
callback: this._onRecordRead,
scope: this
});
},
_onRecordRead: function(record, operation) {
console.log('name:', record.get('Name'));
console.log('test cases:', record.get('TestCases'));
if(operation.wasSuccessful()) {
var testCaseStore = record.getCollection('TestCases', {
autoLoad: true,
listeners: { load: function() {
var testcases = testCaseStore.getRange();
_.each(testcases, function(testcase) {
testCaseStore.remove(testcase);
});
testCaseStore.sync({
callback: function() {
console.log('test cases after removeAll():', record.get('TestCases'));
}
});
testCaseStore.add({'_ref':'/testcase/14469886070'});
testCaseStore.sync({
callback: function() {
console.log('test cases after add():', record.get('TestCases'));
}
});
}}
});
}
},
});
Related
I have a view with multiple collections, implemented like this:
collection: {
folders: new FolderCollection(),
images: new ImageCollection(),
files: new FileCollection()
},
And example collection is like this:
var FolderCollection = Backbone.Collection.extend({
model: folderObj,
initialize:function (){
// this.bindAll(this);
// this.setElement(this.at(0));
},
comparator: function(model) {
return model.get("id");
},
getElement: function() {
return this.currentElement;
},
setElement: function(model) {
this.currentElement = model;
},
next: function (){
this.setElement(this.at(this.indexOf(this.getElement()) + 1));
return this;
},
prev: function() {
this.setElement(this.at(this.indexOf(this.getElement()) - 1));
return this;
}
});
As you can imagine, this View is a display for files, images, and folders. I then populate the view by calling three different functions; one to populate the view with folders, another for files, and another for images. Each of these functions is a separate ajax request. So, because these calls are asynchronous, there's no way to first load folders, then images, then files and there is no consistency when the page loads.
So, my problem is, I need to be able to order these three collections in multiple ways. The first problem is, since the calls are async, sometimes the folders load first, or maybe the files, etc. I can think of two ways to fix this:
Only call the next function after the previous is completed. Is this the best way? If so, how do I do that
After all the collections are loaded, sort them. If so, how is the best way to sort and order multiple collections?
If more code is needed (ie: model or view) please let me know and I can provide what ever is needed.
thanks
jason
EDIT - SHOWING VIEW
var FileManagementView = TemplateView.extend({
viewName: 'fileManagement',
className: 'fileManagement',
events: {
//my events
},
collection: {
folders: new FolderCollection(),
images: new ImageCollection(),
files: new FileCollection()
},
//life cycle
initialize: function (options) {
TemplateView.prototype.initialize.apply(this, [options]);
},
templateContext: function (renderOptions) {
},
postRender: function () {
//more functions to set up the view
this.repopulateViewWithFoldersAndFiles(currentFolderId);
},
template: function (renderOptions) {
return 'MyMainTemplate';
},
repopulateViewWithFoldersAndFiles: function(currentFolderId){
//code to do stuff to create view
//these functions are all async, so theres no way to know what will finish first
this.getFolders(currentFolderId);
this.getImages();
this.getFiles();
},
getFiles: function(){
try{
var that = this;
var url = '?q=url to function';
$.ajax({
url: url,
context: that,
data:{'methodName': 'getFiles'}
}).done(function(data) {
var results = jQuery.parseJSON(data.result.results);
if(results){
$.each(results, function( key, value ) {
var file = new fileObj;
file.set('id', value.id);
file.set('fileName', value.fileName);
//...set more attributes
that.collection.files.add(file);
that.renderFile(file);
});
}
});
} catch(e){
throw e;
}
},
renderFile: function(file){
try{
if(file) {
var template = window.app.getTemplate('AnotherTemplate');
var html = $(template({
id: file.get('id'),
fileName: file.get('fileName'),
fileIconPath: file.get('fileIconPath')
}));
this.$el.find('#fileDropZone').append(html);
}
} catch(e){
throw e;
}
},
getImages: function(){
try{
var url = '?q=url to function';
$.ajax({
url: url,
context: that,
data:{'methodName': 'getImages'}
}).done(function(data) {
var results = jQuery.parseJSON(data.result.results);
if(results){
$.each(results, function( key, value ) {
var image = new imageObj;
image.set('id', value.id);
image.set('imgTitle', value.image_name);
//....set more attributes
that.collection.images.add(image);
that.renderImage(image);
});
}
});
} catch(e){
throw e;
}
},
renderImage: function(image){
try{
if(image) {
var template = window.app.getTemplate('myViewTemplate');
var html = $(template({
imgId: image.get('id'),
imgTitle: image.get('imgTitle'),
//..more attributes
}));
this.$el.find('#fileDropZone').append(html);
}
} catch(e){
throw e;
}
},
getFolders:function(parentId){
var that = this;
var url = '?q=...path to function';
$.ajax({
url: url,
context: that,
data:{'methodName': 'getFolders'}
}).done(function(data) {
var results = jQuery.parseJSON(data.result.results);
if(results){
$.each(results, function( key, value ) {
var folder = new folderObj();
folder.set('folderName', value.folder_name);
folder.set('id', value.folder_id);
//more attributes
that.collection.folders.add(folder);
that.renderFolders(folder);
});
}else{
this.renderFolders(null);
}
});
},
//renders the folders to the view
renderFolders: function(folder){
try{
if(folder) {
var template = window.app.getTemplate('myFolderTemplate');
var html = $(template({
folderId: folder.get('id'),
folderName: folder.get('folderName'),
}));
this.$el.find('#fileDropZone').append(html);
}
} catch(e){
throw e;
}
}
});
What I ended up doing was rewriting my models and creating one model that the others inherit from. Example:
var DataModel =MyBaseModel.extend({
defaults: {
id: null,
//other shared fields
}
});
All my other models inherited, like this:
var folderObj = DataModel.extend({
// Whatever you want in here
urlRoot: '?q=myApp/api/myClassName/',
defaults: {
//other fields here
},
validate: function(attributes){
//validation here
}
});
I then used deferred, which I answered here: Jquery Promise and Defered with returned results
Until now, I have been querying the data stores using Rally App SDK, however, this time I have to update a story using the js sdk. I tried looking up for examples for some sample code that demonstrates how the App SDK can be used to update/add values in Rally. I have been doing CRUD operations using Ruby Rally API but never really did it with the app sdk.
Can anyone provide some sample code or any link to where I could check it out?
Thanks
See this help document on updating and creating reocrds. Below are examples - one updates a story, the other creates a story. There is not much going on in terms of UI: please enable DevTools console to see console.log output.
Here is an example of updating a Defect Collection on a User Story:
Ext.define('CustomApp', {
extend: 'Rally.app.App',
componentCls: 'app',
launch: function() {
console.log("launch");
Rally.data.ModelFactory.getModel({
type: 'User Story',
success: this._onModelRetrieved,
scope: this
});
},
_onModelRetrieved: function(model) {
console.log("_onModelRetrieved");
this.model = model;
this._readRecord(model);
},
_readRecord: function(model) {
var id = 13888228557;
console.log("_readRecord");
this.model.load(id, {
fetch: ['Name', 'Defects'],
callback: this._onRecordRead,
scope: this
});
},
_onRecordRead: function(record, operation) {
console.log('name...', record.get('Name'));
console.log('defects...', record.get('Defects'));
if(operation.wasSuccessful()) {
//load store first by passing additional config to getCollection method
var defectStore = record.getCollection('Defects', {
autoLoad: true,
listeners: { load: function() {
//once loaded now do the add and sync
defectStore.add({'_ref':'/defect/13303315495'});
defectStore.sync({
callback: function() {
console.log('success');
}
});
}}
});
}
},
});
Here is an example of creating a user story, setting a project and scheduling for an iteration:
Ext.define('CustomApp', {
extend: 'Rally.app.TimeboxScopedApp',
componentCls: 'app',
scopeType: 'iteration',
comboboxConfig: {
fieldLabel: 'Select an Iteration:',
labelWidth: 100,
width: 300
},
addContent: function() {
this._getIteration();
},
onScopeChange: function() {
this._getIteration();
},
_getIteration: function() {
var iteration = this.getContext().getTimeboxScope().record.get('_ref');
console.log('iteration',iteration);
if (!this.down('#b2')) {
var that = this;
var cb = Ext.create('Ext.Container', {
items: [
{
xtype : 'rallybutton',
text : 'create',
id: 'b2',
handler: function() {
that._getModel(iteration);
}
}
]
});
this.add(cb);
}
},
_getModel: function(iteration){
var that = this;
Rally.data.ModelFactory.getModel({
type: 'UserStory',
context: {
workspace: '/workspace/12352608129'
},
success: function(model) { //success on model retrieved
that._model = model;
var story = Ext.create(model, {
Name: 'story 777',
Description: 'created via appsdk2'
});
story.save({
callback: function(result, operation) {
if(operation.wasSuccessful()) {
console.log("_ref",result.get('_ref'), ' ', result.get('Name'));
that._record = result;
that._readAndUpdate(iteration);
}
else{
console.log("?");
}
}
});
}
});
},
_readAndUpdate:function(iteration){
var id = this._record.get('ObjectID');
console.log('OID', id);
this._model.load(id,{
fetch: ['Name', 'FormattedID', 'ScheduleState', 'Iteration'],
callback: function(record, operation){
console.log('ScheduleState prior to update:', record.get('ScheduleState'));
console.log('Iteration prior to update:', record.get('Iteration'));
record.set('ScheduleState','In-Progress');
record.set('Iteration', iteration);
record.set('Project', '/project/12352608219')
record.save({
callback: function(record, operation) {
if(operation.wasSuccessful()) {
console.log('ScheduleState after update..', record.get('ScheduleState'));
console.log('Iteration after update..', record.get('Iteration'));
}
else{
console.log("?");
}
}
});
}
})
}
});
I have a model that looks like this:
var TodosModel = Backbone.Model.extend({
defaults: {
id: null,
content: 'Something Todo',
completed: false
},
url: function() { return 'api/'+this.id; }
});
I'm adding models via:
var todoID = _.uniqueId();
var todoContent = this.newTodoField.val();
var todoCompleted = false;
// Ensure there's something to save
if(todoContent.length>0){
var _this = this;
// Create model
var todo = new TodosModel({
id: todoID,
content: todoContent,
completed: todoCompleted
});
todo.save({}, {
wait: true,
success: function(model, response) {
// Let the events deal with rendering...
_this.collection.add(model);
},
error: function(model, response) {
console.log('Could not create todo');
}
});
}
The problem I'm having is that for some reason every id is double incremented - so if I start with no elements I get 1,3,5,7...
Which holds alright, except if I reload and those ID's are brought in from the API, and then the next generated _.uniqueID is based on the count rendered out.
Any help would be greatly appreciated, here's the full code: http://sandbox.fluidbyte.org/todos/js/todos.js
I took the base of this code from a gist. It initially worked perfectly when I first fetch()ed the collection and then in render() called tw-bootstap's .typeahead().
However, I have put in a keypress event to try and restrict the size of the data returned by fetch(). The collection data is returned and it is filtered through prepData() fine and arrives at render(). The typeahead is not working, however at that stage. It may be that the backbone event is overriding render at that point?
// typeahead on the numbers
var Bootstrap = {};
Bootstrap.Typeahead = Backbone.View.extend({
el: '#autocompleteN',
tagName: 'input',
attributes: {"data-provide": "typeahead"},
initialize: function(options){
if(!this.collection) {
return null;
}
//this.collection.on("reset", this.prepData, this);
},
events: {
"keypress": "setSearch"
},
setSearch: _.throttle(function(e) {
var that=this;
var d = e.currentTarget.value;
// strip spaces and remove non-numerics
d = d.replace(/ /g,'');
d = d.replace(/[^0-9]/g, '');
// if it's longer than 2, call a fetch;
if(d.length > 2) {
$.when( app.searchNums.fetch({url: 'api/index.php/search/num/'+d}) ).then(function() {
//console.dir("success");
that.prepData();
});
}
}, 1000),
prepData: function() {
//console.dir("prepData called");
var prepare = _.pluck(this.collection.models, 'attributes');
this.property = this.options.property || _.keys(prepare[0])[0];
this.items = this.options.items;
this.data = _.pluck(prepare, this.property);
this.render();
},
render: function() {
var that = this;
that.$el.typeahead({
source: that.data,
//source: ['PHP', 'MySQL', 'SQL', 'PostgreSQL', 'HTML', 'CSS', 'HTML5', 'CSS3', 'JSON'],
items: that.items,
onselect: function( data ) {
// render the results view here
}
});
return this;
}
});
var bui = new Bootstrap.Typeahead({
collection: app.searchNums,
items: 5
});
Why dont you just set minLength on the typeahead, it looks like that is what you are trying to do?
When I call fetch on my collection the app is calling the server and server returns an array of object. In the success function of the fetch call I've got an empty collection and the original response holding all objects that was responded by the server.
Collection
var OpenOrders = BaseCollection.extend({
model: Order,
url: baseUrl + '/api/orders?status=1'
});
Model
var Order = BaseModel.extend(
{
url:baseUrl + "/api/order",
defaults:{
order_items: new OrderList(),
location: 1,
remark: "remark"
},
initialize: function(options) {
var orderItems = this.get('order_items');
if (orderItems instanceof Array) {
orderItems = new OrderList(orderItems);
this.set({'order_items': orderItems})
}
orderItems.bind('change', _.bind(function() {
this.trigger('change')
}, this))
.bind('remove', _.bind(function() {
this.trigger('change')
}, this));
return this;
},
sum: function() {
return this.get('order_items').sum();
},
validate: function() {
return !!this.get('order_items').length;
},
add:function(product) {
this.get('order_items').add(product);
},
remove: function(product) {
this.get('order_items').remove(product);
}
);
Fetching the collection
this.collection.fetch({success:_.bind( function(collection, response){
console.log('OpenOrdersListView', collection.toJSON())
// logs []
console.log('OpenOrdersListView', response)
// logs [Object, Object ...]
}, this)})
Damm, its the validate method in my model. I've though validate have to return a boolean, but after reading the docs, it has to return an error message only if the model is not valid.
validate: function() {
if (!this.get('order_items').length){
return 'set minium of one product before save the order'
}
},