(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); } }); })();