PageRenderTime 52ms CodeModel.GetById 24ms RepoModel.GetById 0ms app.codeStats 0ms

/static/js/models/stackmodel.js

https://bitbucket.org/gordonbrander/accordion-drawer-prototypes
JavaScript | 524 lines | 295 code | 94 blank | 135 comment | 23 complexity | 4b748386e15edf5debff74ab037de335 MD5 | raw file
Possible License(s): Apache-2.0
  1. define([
  2. 'config',
  3. 'underscore',
  4. 'lib/objecttools',
  5. 'backbone',
  6. 'models/livecollection',
  7. 'models/placecollection',
  8. 'models/thumbnails',
  9. 'models/basemodel',
  10. 'models/eras',
  11. 'lib/promise',
  12. 'lib/bridge',
  13. 'lib/resthelper',
  14. 'lib/lazily',
  15. 'lib/template',
  16. 'logger',
  17. 'lib/errors'
  18. ], function (
  19. config,
  20. util,
  21. objectTools,
  22. Backbone,
  23. LiveCollection,
  24. PlaceCollection,
  25. thumbnails,
  26. BaseModel,
  27. eras,
  28. Promise,
  29. Bridge,
  30. RestHelper,
  31. lazily,
  32. template,
  33. logger,
  34. errors
  35. ) {
  36. var bridge = new Bridge();
  37. var flagError = function(msg){
  38. return function(err) {
  39. logger.error.apply(logger, [msg, err]);
  40. };
  41. };
  42. // Use the model constructor from PlaceCollection.
  43. var SiteModel = PlaceCollection.prototype.model,
  44. __SiteModel = SiteModel.prototype;
  45. var latticeUrl = config.latticeUrl + '{query}';
  46. var tokens = {
  47. latticeRoot: config.latticeRoot,
  48. username: config.username,
  49. service: 'stack'
  50. };
  51. var v2 = { v: 2 };
  52. // A private helper function for creating and adding and search places after
  53. // `success` events. Used below in `StackModel.prototype.processPlace` and
  54. // `StackModel.prototype.processSearchPlace`.
  55. //
  56. // Invoke, passing the `this` context of the current object:
  57. //
  58. // configPlace.call(this, attr);
  59. //
  60. // Attr should be an already-parsed object.
  61. var configPlace = function (attr) {
  62. var siteModel = this.places().get(attr.place_id) || new SiteModel();
  63. siteModel.set(attr, { silent: true });
  64. // If the siteModel is new (hasn't been added to the collection yet),
  65. // add it.
  66. if (!siteModel.collection) this.addPlace(siteModel);
  67. return siteModel;
  68. };
  69. var __BaseModel = BaseModel.prototype;
  70. var StackModel = BaseModel.extend({
  71. idAttribute: 'stack_id',
  72. initialize: function(attrs, options){
  73. if (options) util.extend(this, options);
  74. var urls = this.urls = new RestHelper();
  75. urls.method(
  76. 'fetch',
  77. config.latticeUrl + '/active{query}',
  78. // At this point, we probably don't have an ID to work with.
  79. util.extend({
  80. // Look for stack_id every time the method is invoked -- this way
  81. // we don't need a stack_id when we initialize, only when we fetch.
  82. method: util.bind(this.get, this, 'stack_id')
  83. }, tokens),
  84. v2
  85. );
  86. // Configure a link method for urls.
  87. urls.method('link', latticeUrl, util.extend({
  88. method: 'link'
  89. }, tokens), v2);
  90. // Configure a search method for urls.
  91. urls.method('search', latticeUrl, util.extend({
  92. method: 'search'
  93. }, tokens), v2);
  94. urls.method('social', latticeUrl, util.extend({
  95. method: 'social'
  96. }, tokens), v2);
  97. // Bind our AJAX post-processing handler to AJAX `success` event.
  98. this.bind('success', this.onSuccess, this);
  99. },
  100. // Bound to `success` event in `initialize`. Success fires whenever
  101. // `process()` method is called. `onSuccess` contains various routines
  102. // for processing tangetial info. Typically, you should check for a
  103. // property's existence on the response object, and then forward
  104. // relevant details to a function tasked with processing that information.
  105. onSuccess: function (resp, options) {
  106. // FYI, order of operation currently DOES matter here, since we're
  107. // using `Date.now()` during place creation to sub in for
  108. // `accessed` property, which is not returned during creation.
  109. if (resp.search_place) this.processSearchPlace(resp);
  110. if (resp.profile_place) this.processProfilePlace(resp);
  111. if (resp.place) this.processPlace(resp);
  112. },
  113. url: function (tokens, query) {
  114. return this.urls.fetch(tokens, query);
  115. },
  116. // Issue a warp request to Lattice for this stack.
  117. // At this point, this method is simply a wrapper around `place.save()`.
  118. warp: function (place_id) {
  119. var place = this.fetchPlace(place_id);
  120. Promise.when(place, function (place) {
  121. // Issue a warp request.
  122. // TODO: determine how we can tell new sites apart from previously
  123. // existing ones.
  124. place.save();
  125. }, flagError("stackModel.fetchPlace resulted in an error"));
  126. return place;
  127. },
  128. visit: function(){
  129. // SF:WORK IN PROGRESS:
  130. // register a visit to this stack.
  131. // this is a PUT (update) on the collection that this stack is a part of.
  132. var url = this.collection.url();
  133. $.ajax({
  134. url: url,
  135. type: "PUT",
  136. success: function(resp){
  137. alert("visit registration success\nTODO: stash the session id we were provided, and trigger an event for reaction elsewhere");
  138. },
  139. data: {
  140. place_id: "FIXME: place_id needed",
  141. stack_id: this.id
  142. },
  143. error: flagError("Error response from visit PUT request")
  144. });
  145. },
  146. // Create a hard link to a place within this stack.
  147. // Issues a `POST lattice/:username/stack/link`.
  148. // Automatically adds the resulting SiteModel to
  149. // this StackModel's PlaceCollection.
  150. // Returns a Promise.
  151. link: function (attr, options) {
  152. var required = [
  153. 'place_url',
  154. 'place_title'
  155. ];
  156. options = options || {};
  157. var linkPromise = new Promise();
  158. // Check for required keys.
  159. var missing = objectTools.missing(attr, required);
  160. if (missing.length) throw new errors.KeysMissingError(null, missing);
  161. // Decorate SiteModel with `session_id` and `stack_id`.
  162. // `this.addPlace` also does decoration, but we need to issue a fetch
  163. // before we get the chance to addPlace. Since fetch requires these
  164. // properties, let's go ahead and add them now.
  165. util.defaults(attr, {
  166. session_id: this.get('session_id'),
  167. stack_id: this.get('stack_id')
  168. });
  169. // Create a new siteModel from the attributes provided.
  170. var siteModel = new SiteModel(attr);
  171. // Configure options.
  172. // Get the URL we want to hit. Updated from options.
  173. options.url = this.urls.link(options.tokens, options.query);
  174. // Create a composed handler for resolving promise.
  175. options.success = util.compose(
  176. util.bind(linkPromise.resolve, linkPromise),
  177. util.bind(siteModel.set, siteModel)
  178. );
  179. // Create a bound error handler for rejecting promise.
  180. options.error = util.bind(linkPromise.reject, linkPromise);
  181. // Get the currently active place ID. Do an intelligent fetch of places.
  182. // I.E, won't fetch if it finds the place already in the collection.
  183. Promise.when(
  184. this.places().fetchPlace(this.get('place_id')),
  185. util.bind(function (activePlace) {
  186. // Now that we have the active place, get the up-to-date
  187. // PlaceCollection.
  188. var placeCollection = this.places();
  189. // Get the index of the currently active place -- we want to
  190. // insert after.
  191. var currentAt = placeCollection.indexOf(activePlace);
  192. // When the siteModel comes back with an ID, select it.
  193. Promise.when(linkPromise, util.bind(function (siteModel) {
  194. // Insert new siteModel at appropriate location in `PlaceCollection`.
  195. // When it comes back, it will be decorated with a
  196. // `session_id` and a `stack_id`, making it ready for the
  197. // `link` request.
  198. siteModel = this.addPlace(siteModel, { at: 0 });
  199. placeCollection.selectPlace(siteModel.id);
  200. // Retrieve the thumbnails job that comes back from Lattice.
  201. var thumbnails_job = siteModel.get('thumbnails_job');
  202. // Fake a parse event to get thumbnail job to trigger.
  203. placeCollection.trigger('parse', placeCollection, {
  204. thumbnails_job: thumbnails_job
  205. });
  206. }, this));
  207. // Sync with server via XHR. Make sure the link request is issued as a
  208. // PUT. Our current API thinks of adding places to a stack as PUTing a
  209. // link to an existing stack, rather than POSTing a new place, if you
  210. // know what I mean.
  211. (this.sync || Backbone.sync).call(this, 'update', siteModel, options);
  212. }, this),
  213. function () {
  214. throw new errors.IdDoesNotExistError('Place was not found in stack');
  215. }
  216. );
  217. return linkPromise;
  218. },
  219. // Make a stack search request. Creates a new stack from the following:
  220. //
  221. // * `place_url`
  222. // * `place_title`
  223. // * `search_terms`
  224. // * `search_url`
  225. search: function (attrs, options) {
  226. options = options || {};
  227. var required = [
  228. 'place_url',
  229. 'place_title',
  230. // These will likely get passed in when invoking this method.
  231. 'search_terms',
  232. 'search_url'
  233. ];
  234. var promise = new Promise();
  235. // Set any new properties from attrs.
  236. if (attrs) this.set(attrs);
  237. // Filter down list of required to just those that are missing.
  238. var missing = util.reject(required, this.has, this);
  239. // Check for required attributes. Make some noise if anything
  240. // is missing.
  241. if (missing.length) throw new errors.KeysMissingError(null, missing);
  242. // Create a handler for building new stackmodel and resolving promise.
  243. options.success = util.bind(function (resp) {
  244. var model = this.process(resp, options);
  245. promise.resolve(model);
  246. }, this);
  247. // Create a bound error handler for rejecting promise.
  248. options.error = util.bind(promise.reject, promise);
  249. // URL sent to XHR should be calculated from `urls.search`.
  250. // Update `urls.search` with current search terms.
  251. options.url = this.urls.search(null, {
  252. q: this.get('search_terms')
  253. });
  254. // Sync with our server via XHR. Make sure the search request is
  255. // issued as a POST. Our current API thinks of adding stacks as
  256. // "creating" a search under a particular term.
  257. (this.sync || Backbone.sync).call(this, 'create', this, options);
  258. return promise;
  259. },
  260. social: function (attrs, options) {
  261. options = options || {};
  262. var promisedStack = new Promise();
  263. var required = [
  264. 'service_url',
  265. 'friend_url',
  266. 'friend_name',
  267. 'place_title',
  268. 'place_url'
  269. ];
  270. // Set any new properties from attrs.
  271. if (attrs) this.set(attrs);
  272. // Filter down list of required to just those that are missing.
  273. var missing = util.reject(required, this.has, this);
  274. if (missing.length) throw new errors.KeysMissingError(null, missing);
  275. promisedStack.then(options.success, options.error);
  276. options.success = util.bind(function (resp) {
  277. var stackModel = this.process(resp);
  278. promisedStack.resolve(stackModel);
  279. }, this);
  280. options.error = util.bind(promisedStack.reject, promisedStack);
  281. // URL sent to XHR should be calculated from `urls.search`.
  282. // Update `urls.search` with current search terms.
  283. options.url = this.urls.social();
  284. // Sync with our server via XHR. Make sure the search request is
  285. // issued as a POST. Our current API thinks of adding stacks as
  286. // "creating" a search under a particular term.
  287. (this.sync || Backbone.sync).call(this, 'create', this, options);
  288. return promisedStack;
  289. },
  290. // Places
  291. // -------------------------------------------------------------------------
  292. // Add a place to a `PlaceCollection`.
  293. // Decorates the place on the way in with `session_id` and `stack_id`
  294. // using `decoratePlace` method.
  295. addPlace: function (siteModel, options){
  296. options = options || {};
  297. var placesCollection = this.places();
  298. // Spin up a thumbnails job if necessary.
  299. if (options.thumbnails_job) thumbnails.parse.call(placesCollection, {
  300. thumbnails_job: options.thumbnails_job
  301. });
  302. // Add the siteModel to the array of models.
  303. placesCollection.add(siteModel, options);
  304. // Return the modified siteModel.
  305. return siteModel;
  306. },
  307. fetchPlaces: function(){
  308. var stackModel = this;
  309. var promisedFetch = this.places().fetch({
  310. diff: true
  311. });
  312. var setLoaded = function () {
  313. stackModel.loaded = true;
  314. };
  315. promisedFetch.then(setLoaded, setLoaded);
  316. return promisedFetch;
  317. },
  318. // Fetch an individual place by id. Will refetch places from server
  319. // if ID is not found in current collection. Returns a Promise or value.
  320. //
  321. // TODO: this method is deprecated -- prefer
  322. // `PlaceCollection.prototype.fetchPlace`. Keeping it here for older code
  323. // paths.
  324. fetchPlace: function (place_id) {
  325. return this.places().fetchPlace(place_id);
  326. },
  327. // Create a PlacesCollection if we don't have one already, memoize it.
  328. places: lazily(function () {
  329. return new PlaceCollection([], { stack: this });
  330. }),
  331. // A special-case `PlaceCollection` that contains places which are matched
  332. // in search results and that sort of thing. Expect the places in this
  333. // collection to be frequently changing and frequently empty.
  334. //
  335. // Matches currently do not come back from any single stack API. They only
  336. // come back from stack collection APIs and are manually added to the stack
  337. // instance.
  338. matches: lazily(function () {
  339. return new PlaceCollection([], { stack: this });
  340. }),
  341. // Eras
  342. // -------------------------------------------------------------------------
  343. eras: BaseModel.subcollection('eras', eras.EraCollection),
  344. // Processing methods
  345. // -------------------------------------------------------------------------
  346. // This function is intended to consume *unfiltered* results from the API.
  347. processPlace: function (resp) {
  348. // TODO: if we ever get back > 1 place, we should turn this into a map
  349. // operation.
  350. var place = resp.place;
  351. configPlace.call(this, {
  352. place_id: place.id,
  353. place_title: place.title,
  354. place_url: place.url,
  355. thumbnail_key: place.thumbnail_key,
  356. session_id: resp.session_id,
  357. stack_id: resp.stack.id,
  358. // Accessed time is not handed back from the server for stack searches.
  359. // Fake it with a date object so that we get proper sorting in the
  360. // drawer. This should be roughly accurate, since the server-side hands
  361. // back a Unix Timestamp for accessed.
  362. accessed: Date.now()
  363. });
  364. this.places().selectPlace(place.id);
  365. },
  366. processSearchPlace: function (resp) {
  367. var searchPlace = resp.search_place;
  368. var searchUrl = template(config.searchResults);
  369. configPlace.call(this, {
  370. place_id: searchPlace.id,
  371. place_title: 'Search: ' + resp.stack.title,
  372. place_url: searchUrl({ query: resp.stack.title, provider: 'bing' }),
  373. session_id: resp.session_id,
  374. stack_id: resp.stack.id,
  375. thumbnail_key: searchPlace.thumbnail_key,
  376. accessed: Date.now()
  377. });
  378. },
  379. // Handles creating a place from `resp.profile_place`.
  380. processProfilePlace: function (resp) {
  381. var place = resp.profile_place;
  382. configPlace.call(this, {
  383. place_id: place.id,
  384. place_title: resp.stack.title,
  385. // Get the place_url from the `friend_url` on this `StackModel`.
  386. // `friend_url` is a required field for issuing a `social` request,
  387. // so it should be safe to assume it's available on the instance.
  388. //
  389. // NOTE: this will not work in the `views` iframe-based app, because
  390. // of iframe access blocks on <http://twitter.com>
  391. // and <http://facebook.com>.
  392. place_url: this.get('friend_url'),
  393. session_id: resp.session_id,
  394. stack_id: resp.stack.id,
  395. thumbnail_key: place.thumbnail_key,
  396. accessed: Date.now()
  397. });
  398. },
  399. // PSA: **AVOID `this`** in `StackModel.parse`. If we use `this`, we can not
  400. // map over `StackModel.parse` in `StackCollection.parse`.
  401. parse: function (resp) {
  402. var stack = resp.stack,
  403. place = resp.place,
  404. page_count,
  405. model = {};
  406. if(place){
  407. util.extend(model, {
  408. place_id: place.id,
  409. place_title: place.title,
  410. place_url: place.url
  411. });
  412. }
  413. if(stack) {
  414. util.extend(model, {
  415. stack_id: stack.id,
  416. stack_title: stack.title
  417. });
  418. // Include page count if it has been returned.
  419. if (stack.page_count) {
  420. model.page_count = stack.page_count;
  421. }
  422. }
  423. if(resp.session_id) {
  424. model.session_id = resp.session_id;
  425. }
  426. return model;
  427. }
  428. });
  429. return StackModel;
  430. });