(function() {
var Ext = window.Ext4 || window.Ext;
var wsapiMaxPageSize = 200;
/**
* A data store which can retrieve data from Rally's WSAPI.
* See the [Data Stores](#!/guide/data_stores) guide for more information on working with web services.
*
* Ext.create('Rally.data.wsapi.Store', {
* model: 'User Story',
* autoLoad: true,
* listeners: {
* load: function(store, data, success) {
* //process data
* }
* },
* fetch: ['Name', 'ScheduleState']
* });
*
* The store accepts either a string or {@link Rally.data.wsapi.Model} representing the type to retrieve.
* The store supports the standard Ext store config properties for filtering and sorting.
*
* Ext.create('Rally.data.wsapi.Store', {
* model: userStoryModel,
* fetch: true,
* autoLoad: true,
* filters: [
* {
* property: 'ScheduleState',
*
* value: 'Accepted'
* }
* ],
* sorters: [
* {
* property: 'FormattedID',
* direction: 'ASC'
* }
* ]
* });
*
* Scoping can be controlled via the {@link Rally.data.wsapi.Store#context context} config parameter.
* This can be an object literal or if running in an app can be obtained via the {@link Rally.app.App#getContext} method.
*
* Ext.create('Rally.data.wsapi.Store', {
* model: userStoryModel,
* context: {
* project: '/project/1234',
* projectScopeUp: false,
* projectScopeDown: false
* },
* autoLoad: true
* });
*
*
*/
Ext.define('Rally.data.wsapi.Store', {
// Client Metrics Note: WsapiDataStore is too low level to record its load begins/ends. The problem is
// client metrics can only reliable keep track of one load per component at a time. WsapiDataStore makes
// no guarantee that only one load will happen at a time. It's better to measure the component that is using
// the store. All is not lost, the actual data requests that WsapiDataStore makes *are* measured by client metrics.
requires: ['Rally.data.ModelFactory', 'Rally.data.Ranker'],
extend: 'Rally.data.PageableStore',
alias: ['store.rallywsapidatastore', 'store.rallywsapistore'],
alternateClassName: ['Rally.data.WsapiDataStore'],
mixins: {
messageable: 'Rally.Messageable',
findRecords: 'Rally.data.WsapiFindRecords',
recordUpdatable: 'Rally.data.wsapi.RecordUpdatable'
},
statics: {
wsapiMaxPageSize: wsapiMaxPageSize
},
/**
* @cfg {Boolean} autoSync
* NOT RECOMMENDED with any Rally store. Defaults to false. Editing and pagination will not work when set to true.
*/
autoSync: false,
/**
* @cfg {Number}
* The number of records to retrieve per page.
*/
pageSize: wsapiMaxPageSize,
/**
* @cfg {String[]}
* The fields to be retrieved
*/
fetch: undefined,
/**
* @cfg {Boolean} useShallowFetch
* When enabled, the fields defined by {@link #fetch} will populate the shallowFetch request parameter instead of fetch.
*/
useShallowFetch: false,
/**
* @cfg {Boolean} enablePostGet
* When enabled, store load will set enablePostGet on the operation sent to the proxy. Proxy will turn this
* GET into a POST?_method=GET. Handy when GET url is too long (resulting in a 413)
*/
enablePostGet: false,
/**
* @private
* @cfg {Boolean} compact
* Passed through to the proxy. Turns on/off WSAPI results compaction. If undefined, it uses the proxy's default mode.
*/
compact: undefined,
/**
* @cfg {Boolean} includeSchema
* When enabled, the schema will come down with the results.
*/
includeSchema: false,
/**
* The data scoping to be applied.
* @cfg {Object} context
* @cfg {String} context.workspace The ref of the workspace to scope to
* @cfg {String} context.project The ref of the project to scope to. Specify null to query the entire specified workspace.
* @cfg {Boolean} context.projectScopeUp Whether to scope up the project hierarchy
* @cfg {Boolean} context.projectScopeDown Whether to scope down the project hierarchy
*/
context: undefined,
/**
* The wsapi version to use when automatically retrieving a model before loading.
* By default the wsapi version of the specified model is used.
* @cfg {String/Number} wsapiVersion
*/
wsapiVersion: undefined,
/**
* @private
* @cfg {String}
* The search string
* Search can be used on its own or combined with filters.
* When used on its own, search behaves like the search box in Rally.
* When used with filters, search is evaluated first and then filters are applied to further narrow the results.
*/
search: undefined,
/**
* @cfg {Boolean}
* Automatically translate rank-like fields to the appropriate field for
* this store's model type in fetch and order params.
* Set to false to maintain the real ultimate power to configure these.
*/
enableRankFieldParameterAutoMapping: true,
constructor: function(config) {
if (config.proxy) {
this._customProxyInUse = true;
} else {
//If a string model type was specified
//construct a default proxy for now
if (Ext.isString(config.model)) {
config.proxy = Rally.environment.getIoProvider().getProxy({
wsapiVersion: config.wsapiVersion
});
}
}
this.callParent(arguments);
},
/**
* @inheritdoc
*
* @returns Deft.Promise
*/
load: function(options) {
this.loading = true;
options = options || {};
if (Ext.isFunction(options)) {
options = {
callback: options
};
}
//Load the model first if a string was specified
if (!this.model && Ext.isString(this.initialConfig.model)) {
return this._hydrateModelAndLoad(options);
} else {
var promise = this._wrapCallbackWithPromise(options);
this._setFetch(options);
options.useShallowFetch = this.useShallowFetch;
options.fetch = this.fetch;
options.search = this.search;
options.context = this.context;
options.requester = options.requester || this.requester;
options.enablePostGet = this.enablePostGet;
options.includeSchema = this.includeSchema;
options.compact = this.compact;
if (this.url) {
options.url = this.url;
}
this.callParent([options]);
return promise;
}
},
/**
* @inheritdoc
* @returns {Deft.Promise(Ext.data.Batch)}
*/
sync: function(options) {
options = options || {};
var deferred = Ext.create('Deft.Deferred'),
me = this,
opts = Ext.apply({}, {
callback: function(batch, syncOptions) {
Ext.callback(options.callback, options.scope || me, [batch, syncOptions]);
if (!batch.hasException) {
deferred.resolve(batch);
} else {
deferred.reject(batch);
}
}
}, options);
this.callParent([opts]);
return deferred.promise;
},
/**
* Sets the filter without loading or locally filtering data set.
*
* @param filter {Object|Object[]} Filters to set.
*/
setFilter: function(filter) {
this.clearFilter(true);
_.each(this.decodeFilters(Ext.Array.from(filter)), function(filter) {
this.filters.add(filter);
}, this);
},
/**
* Reload the specified record. The current store filters will also be applied.
* @param {Rally.data.wsapi.Model/Number} record the record or id of the record to reload
* @param {Object} options additional options to be applied to the {Ext.data.Operation}.
* @param {Function} options.success callback - @deprecated - use returned promise instead
* @param {Function} options.failure callback - @deprecated - use returned promise instead
* @param {Object} options.scope callback scope - @deprecated - use returned promise instead
* @return {Deft.Promise(Rally.data.wsapi.Model)}
*/
reloadRecord: function(record, options) {
options = options || {};
var deferred = Ext.create('Deft.promise.Deferred');
var operation = Ext.create('Ext.data.Operation', Ext.merge({
action: 'read',
filters: this.filters.getRange().concat({
property: 'ObjectID',
value: record.getId ? record.getId() : record
}),
limit: 1,
enablePostGet: this.enablePostGet,
requester: this,
context: this.context
}, options));
operation.useShallowFetch = this.useShallowFetch;
operation.fetch = this.fetch || this._getFetchFields(this.model);
this.model.getProxy().read(operation, function(op) {
if(op.wasSuccessful() && op.getRecords() && op.getRecords().length) {
var record = op.getRecords()[0];
Ext.callback(options.success, options.scope, [record]);
deferred.resolve(record);
} else {
Ext.callback(options.failure, options.scope, [op]);
deferred.reject(op);
}
}, this);
return deferred.promise;
},
hydrateModel: function() {
return Rally.data.ModelFactory.getModel({
type: this.initialConfig.model,
context: this.context,
wsapiVersion: this.wsapiVersion,
requester: this.requester
}).then({
success: function(model) {
this.model = model;
if (this._customProxyInUse) {
this.proxy.setModel(model);
} else {
this.proxy = model.getProxy();
}
return model;
},
failure: function(msg) {
Ext.Error.raise(msg);
return msg;
},
scope: this
});
},
sums: {},
getSums: function() {
return this.sums;
},
setSums: function(sums) {
this.sums = sums;
},
_wrapCallbackWithPromise: function(options) {
var me = this;
var deferred = new Deft.Deferred(),
promise = deferred.promise,
originalCallback = options.callback;
options.callback = function (records, operation, success) {
if (operation.resultSet && operation.resultSet.sums) {
me.setSums(operation.resultSet.sums);
}
if(Ext.isFunction(originalCallback)){
originalCallback.apply(options.scope || me, arguments);
}
me._cleanUpCallbackForReload(options, originalCallback);
me._cleanUpCallbackForReload(me.lastOptions, originalCallback);
if (success) {
deferred.resolve(records, operation);
} else {
deferred.reject(operation);
}
deferred = null;
options = null;
me = null;
};
return promise;
},
_cleanUpCallbackForReload: function(options, callback) {
if (options) {
options.callback = callback;
}
},
_hydrateModelAndLoad: function(options){
var deferred = new Deft.Deferred();
this.hydrateModel().then({
success: function(model) {
//Call load with original options
this.load(options).then({
success: Ext.bind(deferred.resolve, deferred),
failure: Ext.bind(deferred.reject, deferred)
});
},
scope: this
});
return deferred.promise;
},
_getModelFailureMessage: function(config) {
return 'Invalid model type specified: ' + config.model;
},
/**
* @private
*/
filter: function() {
//reset page to the first page
this.currentPage = 1;
this.callParent(arguments);
},
/**
* @private
* Override to add fetch param.
*/
prefetch: function(options) {
options = options || {};
options.fetch = this.fetch.join(',');
return this.callParent([options]);
},
/**
* @private
* override to turn off remote filtering when clearing filters
*/
clearFilter: function(suppressEvent, suppressRemoteFilter) {
var remoteFilter = this.remoteFilter;
this.remoteFilter = suppressRemoteFilter ? false : remoteFilter;
try {
var results = this.callParent(arguments);
return results;
} finally {
this.remoteFilter = remoteFilter;
}
},
/**
* @private
* Returns all the items currently in the store. This method returns them in their "raw" form, as simple
* objects in the same format as they were sent from the server. Use #getRecords to retrieve all the items
* as proper instances of {@link Rally.data.Model}.
*
* NOTE: this method is deprecated, use #getRecords instead
*
* @return {Array} all of the items currently in the store
*/
getItems: function() {
return Ext.Array.pluck(this.getRange(), 'raw');
},
/**
* Returns all the items currently in the store. Returns the items as instances of {@link Rally.data.Model}.
* Use #getItems to retrieve the data in its raw form.
*
* @return {Rally.data.Model[]} The records currently in the store
*/
getRecords: function() {
var items = [];
this.each(function(storeItem) {
items.push(storeItem);
});
return items;
},
/**
* Collect all records from the requested pages
* @param {Object} options
* @param {Number} options.startPage defaults to the first page
* @param {Number} options.endPage defaults to the last page
* @param {Function} options.callback
* @param {Object} options.scope
*/
loadPages: function(options) {
var collectedRecords = [];
var totalCount, pageCount;
var pageNum = options.startPage || 1;
var nextPage = Ext.bind(function() {
if (options.endPage && pageNum > options.endPage) {
return Ext.callback(options.callback, options.scope, [collectedRecords]);
}
this.loadPage(pageNum, {
callback: function(records) {
//we have to wait until we load a page to ensure that the total count is available
totalCount = totalCount || this.getTotalCount();
pageCount = pageCount || Math.ceil(totalCount / this.pageSize) || 1;
options.endPage = options.endPage || pageCount;
collectedRecords = collectedRecords.concat(records);
pageNum++;
nextPage();
},
scope: this
});
}, this);
nextPage();
},
/**
* Create a new store using this store's initial configuration
*/
clone: function() {
var cloneConfig = Ext.merge(this.initialConfig, {sorters: this.getSorters()});
return Ext.create(this.self, cloneConfig);
},
/**
* @inheritdoc Rally.data.WsapiFindRecords.findExact
*/
findExact: function(field, value, start) {
return this.mixins.findRecords.findExact.call(this, field, value, start);
},
/**
* Finds a record in the store that matches the given parameters.
* @param {Rally.data.wsapi.Model/Object/String} field Record to find or field of the record used to look up by value
* If record or object is passed, the _ref field or property, respectively, will be used to search by
* @param {Object} [value] The value of the field to find a matching record with
*/
findExactRecord: function(field, value) {
if (value) {
return this.findRecord(field, value, undefined, undefined, undefined, true);
} else {
try {
var ref = Rally.util.Ref.getRefObject(field);
return this.findExactRecord('_ref', ref.getRelativeUri());
} catch (e) {} // not a ref
}
return null;
},
/**
* Finds a record in the store that matches the given parameters
*
* Unlike the find methods provided by Ext, this method ignores filters and it also performs only exact matches.
*
* @param {String} Name of the field to match against
* @param {Object} [value] The value of the field to find a matching record with
*/
findRecordEvenIfFiltered: function(field, value) {
if (this.isFiltered() && this.snapshot && this.snapshot.items) {
var match = _.find(this.snapshot.items, function(item) {
return item.get(field) === value;
});
return typeof match !== 'undefined' ? match : null;
}
return this.findExactRecord(field, value);
},
_setFetch: function(options) {
this.fetch = options.fetch ||this.fetch || this._getFetchFields(this.model);
if(this.enableRankFieldParameterAutoMapping) {
this._convertFetch();
this._convertSorters();
}
},
_getFetchFields: function (model) {
return _.pluck(_.isFunction(model.getNonCollectionFields) ? model.getNonCollectionFields() : [], 'name');
},
_convertFetch: function() {
var fetch = this.fetch;
if (Ext.isString(fetch)) {
fetch = fetch.split(',');
}
if (Ext.isArray(fetch)) {
this.fetch = _.uniq(
_.map(fetch, function (item) {
if (Rally.data.Ranker.isRankField(item)) {
item = Rally.data.Ranker.getRankField(this.model);
}
return item;
}, this)
);
}
},
_convertSorters: function() {
this.sorters.each(function (sorter) {
if (Rally.data.Ranker.isRankField(sorter.property)) {
sorter.property = Rally.data.Ranker.getRankField(this.model);
}
}, this);
}
});
})();