/pancake-web/pancake/web/static/js/models/stackmodel.js
JavaScript | 631 lines | 361 code | 115 blank | 155 comment | 35 complexity | 331fd008dfd0207fd3252fc3cf747a28 MD5 | raw file
Possible License(s): MPL-2.0-no-copyleft-exception, LGPL-2.1, MIT, Apache-2.0
- /* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
- define([
- 'config',
- 'underscore',
- 'lib/objecttools',
- 'backbone',
- 'models/placecollection',
- 'models/thumbnails',
- 'models/basemodel',
- 'models/eras',
- 'lib/promise',
- 'lib/bridge',
- 'lib/resthelper',
- 'lib/lazily',
- 'lib/template',
- 'logger',
- 'lib/errors'
- ], function (
- config,
- util,
- objectTools,
- Backbone,
- PlaceCollection,
- thumbnails,
- BaseModel,
- eras,
- Promise,
- Bridge,
- RestHelper,
- lazily,
- template,
- logger,
- errors
- ) {
- var bridge = new Bridge();
- var flagError = function(msg){
- return function(err) {
- logger.error.apply(logger, [msg, err]);
- };
- };
- // Use the model constructor from PlaceCollection.
- var SiteModel = PlaceCollection.prototype.model,
- __SiteModel = SiteModel.prototype;
- var latticeUrl = config.latticeUrl + '{query}';
- var tokens = {
- latticeRoot: config.latticeRoot,
- username: config.username,
- service: 'stack'
- };
- var v2 = { v: 2 };
- // A private helper function for creating and adding and search places after
- // `success` events. Used below in `StackModel.prototype.processPlace` and
- // `StackModel.prototype.processSearchPlace`.
- //
- // Invoke, passing the `this` context of the current object:
- //
- // configPlace.call(this, attr);
- //
- // Attr should be an already-parsed object.
- var configPlace = function (attr) {
- var siteModel = this.places().get(attr.place_id) || new SiteModel();
- siteModel.set(attr, { silent: true });
- // If the siteModel is new (hasn't been added to the collection yet),
- // add it.
- if (!siteModel.collection) this.addPlace(siteModel);
- return siteModel;
- };
- var __BaseModel = BaseModel.prototype;
- var StackModel = BaseModel.extend({
- idAttribute: 'stack_id',
-
- initialize: function(attrs, options){
- if (options) util.extend(this, options);
- var urls = this.urls = new RestHelper();
- urls.method(
- 'fetch',
- config.latticeUrl + '/active{query}',
- // At this point, we probably don't have an ID to work with.
- util.extend({
- // Look for stack_id every time the method is invoked -- this way
- // we don't need a stack_id when we initialize, only when we fetch.
- method: util.bind(this.get, this, 'stack_id')
- }, tokens),
- v2
- );
- // Configure a link method for urls.
- urls.method('link', latticeUrl, util.extend({
- method: 'link'
- }, tokens), v2);
- // Configure a search method for urls.
- urls.method('search', latticeUrl, util.extend({
- method: 'search'
- }, tokens), v2);
- urls.method('social', latticeUrl, util.extend({
- method: 'social'
- }, tokens), v2);
- urls.method('direct', latticeUrl, util.extend({
- method: 'direct'
- }, tokens), v2);
- // /lattice/:username/session/active/:session_id/close
- urls.remove = function(urlTokens, query){
- urlTokens = urlTokens || {};
- util.extend(urlTokens, tokens, {
- service: 'session/active',
- method: 'close'
- });
-
- // {latticeRoot}/{username}/{service}/{method} -> {latticeRoot}/{username}/{service}/{session_id}{method}
- var urlTemplate = config.latticeUrl.replace('{method}', '{session_id}/{method}');
- return template(urlTemplate)(urlTokens, util.defaults(query || {}, v2));
- };
- // Bind our AJAX post-processing handler to AJAX `success` event.
- this.bind('success', this.onSuccess, this);
- },
- // Bound to `success` event in `initialize`. Success fires whenever
- // `process()` method is called. `onSuccess` contains various routines
- // for processing tangetial info. Typically, you should check for a
- // property's existence on the response object, and then forward
- // relevant details to a function tasked with processing that information.
- onSuccess: function (resp, options) {
- // FYI, order of operation currently DOES matter here, since we're
- // using `Date.now()` during place creation to sub in for
- // `accessed` property, which is not returned during creation.
- if (resp.search_place) this.processSearchPlace(resp);
- if (resp.profile_place) this.processProfilePlace(resp);
- if (resp.place) this.processPlace(resp);
- },
- set: function (attrs, options) {
- // Parse out places, pass to `places()`.
- if (attrs.places) {
- // We trust that `StackModel.parse` has transformed places into a JSON
- // representation that is compatible with `this.places`.
- this.places().add(attrs.places);
- // Remove places reference from object.
- delete attrs.places;
- }
- return __BaseModel.set.call(this, attrs, options);
- },
- url: function (tokens, query) {
- return this.urls.fetch(tokens, query);
- },
- // Issue a warp request to Lattice for this stack.
- // At this point, this method is simply a wrapper around `place.save()`.
- warp: function (place_id) {
- var place = this.fetchPlace(place_id);
- return Promise.whenPromise(place, function (place) {
- // Issue a warp request.
- // TODO: determine how we can tell new sites apart from previously
- // existing ones.
- place.save();
- }, flagError("stackModel.fetchPlace resulted in an error"));
- },
- visit: function(){
- // SF:WORK IN PROGRESS:
- // register a visit to this stack.
- // this is a PUT (update) on the collection that this stack is a part of.
- var url = this.collection.url();
- $.ajax({
- url: url,
- type: "PUT",
- success: function(resp){
- alert("visit registration success\nTODO: stash the session id we were provided, and trigger an event for reaction elsewhere");
- },
- data: {
- place_id: "FIXME: place_id needed",
- stack_id: this.id
- },
- error: flagError("Error response from visit PUT request")
- });
- },
- // Create a hard link to a place within this stack.
- // Issues a `POST lattice/:username/stack/link`.
- // Automatically adds the resulting SiteModel to
- // this StackModel's PlaceCollection.
- // Returns a Promise.
- link: function (attr, options) {
- var required = [
- 'place_url',
- 'place_title'
- ];
- options = options || {};
- var linkPromise = new Promise();
- // Check for required keys.
- var missing = objectTools.missing(attr, required);
- if (missing.length) throw new errors.KeysMissingError(null, missing);
- // Decorate SiteModel with `session_id` and `stack_id`.
- // `this.addPlace` also does decoration, but we need to issue a fetch
- // before we get the chance to addPlace. Since fetch requires these
- // properties, let's go ahead and add them now.
- util.defaults(attr, {
- session_id: this.get('session_id'),
- stack_id: this.get('stack_id')
- });
-
- // Create a new siteModel from the attributes provided.
- var siteModel = new SiteModel(attr);
- // Configure options.
- // Get the URL we want to hit. Updated from options.
- options.url = this.urls.link(options.tokens, options.query);
- // Create a composed handler for resolving promise.
- options.success = util.compose(
- util.bind(linkPromise.resolve, linkPromise),
- util.bind(siteModel.set, siteModel)
- );
- // Create a bound error handler for rejecting promise.
- options.error = util.bind(linkPromise.reject, linkPromise);
- // Get the currently active place ID. Do an intelligent fetch of places.
- // I.E, won't fetch if it finds the place already in the collection.
- Promise.when(
- this.places().fetchPlace(this.get('place_id')),
- util.bind(function (activePlace) {
- // Now that we have the active place, get the up-to-date
- // PlaceCollection.
- var placeCollection = this.places();
- // Get the index of the currently active place -- we want to
- // insert after.
- var currentAt = placeCollection.indexOf(activePlace);
- // When the siteModel comes back with an ID, select it.
- Promise.when(linkPromise, util.bind(function (siteModel) {
- // Insert new siteModel at appropriate location in `PlaceCollection`.
- // When it comes back, it will be decorated with a
- // `session_id` and a `stack_id`, making it ready for the
- // `link` request.
- siteModel = this.addPlace(siteModel, { at: 0 });
- placeCollection.selectPlace(siteModel.id);
- // Retrieve the thumbnails job that comes back from Lattice.
- var thumbnails_job = siteModel.get('thumbnails_job');
- // Fake a parse event to get thumbnail job to trigger.
- placeCollection.trigger('parse', placeCollection, {
- thumbnails_job: thumbnails_job
- });
- }, this));
- // Sync with server via XHR. Make sure the link request is issued as a
- // PUT. Our current API thinks of adding places to a stack as PUTing a
- // link to an existing stack, rather than POSTing a new place, if you
- // know what I mean.
- (this.sync || Backbone.sync).call(this, 'update', siteModel, options);
- }, this),
- function () {
- throw new errors.IdDoesNotExistError('Place was not found in stack');
- }
- );
-
- return linkPromise;
- },
- // Make a stack search request. Creates a new stack from the following:
- //
- // * `place_url`
- // * `place_title`
- // * `search_terms`
- // * `search_url`
- search: function (attrs, options) {
- options = options || {};
- var required = [
- 'place_url',
- 'place_title',
- // These will likely get passed in when invoking this method.
- 'search_terms',
- 'search_url'
- ];
- var promise = new Promise();
- // Set any new properties from attrs.
- if (attrs) this.set(attrs);
- // Filter down list of required to just those that are missing.
- var missing = util.reject(required, this.has, this);
- // Check for required attributes. Make some noise if anything
- // is missing.
- if (missing.length) throw new errors.KeysMissingError(null, missing);
- // Create a handler for building new stackmodel and resolving promise.
- options.success = util.bind(function (resp) {
- var model = this.process(resp, options);
- promise.resolve(model);
- }, this);
- // Create a bound error handler for rejecting promise.
- options.error = function(err){
- promise.reject(err, Boolean("dont throw on error"));
- };
- // URL sent to XHR should be calculated from `urls.search`.
- // Update `urls.search` with current search terms.
- options.url = this.urls.search(null, {
- q: this.get('search_terms') // leave the 'search' resthelper to do the url encoding of values
- });
- // Sync with our server via XHR. Make sure the search request is
- // issued as a POST. Our current API thinks of adding stacks as
- // "creating" a search under a particular term.
- (this.sync || Backbone.sync).call(this, 'create', this, options);
- return promise;
- },
- social: function (attrs, options) {
- options = options || {};
- var promisedStack = new Promise();
- var required = [
- 'service_url',
- 'friend_url',
- 'friend_name',
- 'place_title',
- 'place_url'
- ];
- // Set any new properties from attrs.
- if (attrs) this.set(attrs);
- // Filter down list of required to just those that are missing.
- var missing = util.reject(required, this.has, this);
- if (missing.length) throw new errors.KeysMissingError(null, missing);
- promisedStack.then(options.success, options.error);
- options.success = util.bind(function (resp) {
- var stackModel = this.process(resp, { silent: true }); // get back the stack model, but *dont* trigger events
- promisedStack.resolve(stackModel);
- }, this);
- options.error = util.bind(promisedStack.reject, promisedStack);
- // URL sent to XHR should be calculated from `urls.search`.
- // Update `urls.search` with current search terms.
- options.url = this.urls.social();
- // Sync with our server via XHR. Make sure the search request is
- // issued as a POST. Our current API thinks of adding stacks as
- // "creating" a search under a particular term.
- (this.sync || Backbone.sync).call(this, 'create', this, options);
- return promisedStack;
- },
- direct: function (attrs, options) {
- options = options || {};
- var promisedStack = new Promise();
- var required = [
- 'url',
- 'title'
- ];
- // Set any new properties from attrs.
- if (attrs) this.set(attrs);
- // Filter down list of required to just those that are missing.
- var missing = util.reject(required, this.has, this);
- if (missing.length) throw new errors.KeysMissingError(null, missing);
- promisedStack.then(options.success, options.error);
- options.success = util.bind(function (resp) {
- var stackModel = this.process(resp);
- promisedStack.resolve(stackModel);
- }, this);
- options.error = util.bind(promisedStack.reject, promisedStack);
- // URL sent to XHR should be calculated from `urls.search`.
- // Update `urls.search` with current search terms.
- options.url = this.urls.direct();
- // Sync with our server via XHR. Make sure the search request is
- // issued as a POST. Our current API thinks of adding stacks as
- // "creating" a search under a particular term.
- (this.sync || Backbone.sync).call(this, 'create', this, options);
- return promisedStack;
- },
-
- remove: function(options){
- options = options || {};
- var promise = new Promise();
- var cid = this.cid;
- var model = this;
- var collection = this.collection;
-
- promise.then(options.success || function(){}, options.error || function(){});
- options.success = util.bind(function (resp) {
- // is the model automatically removed from the collection?
- collection.remove([model]);
- promise.resolve(true);
- }, this);
- options.error = function(){
- var model = collection.getByCid(cid);
- if(model) {
- logger.log("Remove stack failed, " + model.get('stack_title') + " still exists in collection");
- }
- promise.reject(false);
- };
- // URL sent to XHR should be calculated from `urls.remove`.
- options.url = this.urls.remove({ session_id: model.get('session_id') });
- // Issue the POST to our server via XHR
- (this.sync || Backbone.sync).call(this, 'create', this, options);
- return promise;
- },
- // Places
- // -------------------------------------------------------------------------
- // Add a place to a `PlaceCollection`.
- // Decorates the place on the way in with `session_id` and `stack_id`
- // using `decoratePlace` method.
- addPlace: function (siteModel, options){
- options = options || {};
- var placesCollection = this.places();
- // Spin up a thumbnails job if necessary.
- if (options.thumbnails_job) thumbnails.parse.call(placesCollection, {
- thumbnails_job: options.thumbnails_job
- });
- // Add the siteModel to the array of models.
- placesCollection.add(siteModel, options);
- // Return the modified siteModel.
- return siteModel;
- },
- fetchPlaces: function(){
- var stackModel = this;
- var promisedFetch = this.places().fetch({
- diff: true
- });
- var setLoaded = function () {
- stackModel.loaded = true;
- };
-
- promisedFetch.then(setLoaded, setLoaded);
- return promisedFetch;
- },
- // Fetch an individual place by id. Will refetch places from server
- // if ID is not found in current collection. Returns a Promise or value.
- //
- // TODO: this method is deprecated -- prefer
- // `PlaceCollection.prototype.fetchPlace`. Keeping it here for older code
- // paths.
- fetchPlace: function (place_id) {
- return this.places().fetchPlace(place_id);
- },
- // Create a PlacesCollection if we don't have one already, memoize it.
- places: BaseModel.subcollection('places', function () {
- return new PlaceCollection(undefined, { stack: this });
- }),
- // A special-case `PlaceCollection` that contains places which are matched
- // in search results and that sort of thing. Expect the places in this
- // collection to be frequently changing and frequently empty.
- //
- // Matches currently do not come back from any single stack API. They only
- // come back from stack collection APIs and are manually added to the stack
- // instance.
- matches: BaseModel.subcollection('matches', function () {
- return new PlaceCollection([], { stack: this });
- }),
- // Processing methods
- // -------------------------------------------------------------------------
- // This function is intended to consume *unfiltered* results from the API.
- processPlace: function (resp) {
- // TODO: if we ever get back > 1 place, we should turn this into a map
- // operation.
- var place = resp.place;
- configPlace.call(this, {
- place_id: place.id,
- place_title: place.title,
- place_url: place.url,
- thumbnail_key: place.thumbnail_key,
- session_id: resp.session_id,
- stack_id: resp.stack.id,
- // Accessed time is not handed back from the server for stack searches.
- // Fake it with a date object so that we get proper sorting in the
- // drawer. This should be roughly accurate, since the server-side hands
- // back a Unix Timestamp for accessed.
- accessed: Date.now()
- });
- this.places().selectPlace(place.id);
- },
- processSearchPlace: function (resp) {
- var searchPlace = resp.search_place;
- var searchUrl = template(config.searchResults);
- configPlace.call(this, {
- place_id: searchPlace.id,
- place_title: 'Search: ' + resp.stack.title,
- place_url: searchUrl({ query: resp.stack.title, provider: 'bing' }),
- session_id: resp.session_id,
- stack_id: resp.stack.id,
- thumbnail_key: searchPlace.thumbnail_key,
- accessed: Date.now()
- });
- },
- // Handles creating a place from `resp.profile_place`.
- processProfilePlace: function (resp) {
- var place = resp.profile_place;
- configPlace.call(this, {
- place_id: place.id,
- place_title: resp.stack.title,
- // Get the place_url from the `friend_url` on this `StackModel`.
- // `friend_url` is a required field for issuing a `social` request,
- // so it should be safe to assume it's available on the instance.
- //
- // NOTE: this will not work in the `views` iframe-based app, because
- // of iframe access blocks on <http://twitter.com>
- // and <http://facebook.com>.
- place_url: this.get('friend_url'),
- session_id: resp.session_id,
- stack_id: resp.stack.id,
- thumbnail_key: place.thumbnail_key,
- accessed: Date.now()
- });
- },
- // PSA: **AVOID `this`** in `StackModel.parse`. If we use `this`, we can not
- // map over `StackModel.parse` in `StackCollection.parse`.
- parse: function (resp) {
- var stack = resp.stack,
- place = resp.place,
- places = resp.places,
- page_count,
- model = {};
- if(stack) {
- util.extend(model, {
- stack_id: stack.id,
- stack_title: stack.title,
- // Subtype is used to customize the icon shown for the stack.
- subtype: stack.subtype
- });
- // Include page count if it has been returned.
- if (stack.page_count) {
- model.page_count = stack.page_count;
- }
- }
- if(resp.session_id) {
- model.session_id = resp.session_id;
- }
- if (place) {
- var placeModel = {
- place_id: place.id,
- place_title: place.title,
- place_url: place.url
- };
- util.extend(model, placeModel, {
- places: [
- // If this item was included, it means it is the active place for
- // the stack -- so it only makes sense to highlight it.
- util.extend({ selected: true }, placeModel)
- ]
- });
- }
- return model;
- }
- });
-
- return StackModel;
- });