PageRenderTime 139ms CodeModel.GetById 42ms RepoModel.GetById 10ms app.codeStats 0ms

/pancake-web/pancake/web/static/js/models/searchcollections.js

https://bitbucket.org/mozillapancake/pancake
JavaScript | 343 lines | 189 code | 43 blank | 111 comment | 15 complexity | 5b69741e88e4d9e2e479c63b9faa7a28 MD5 | raw file
Possible License(s): MPL-2.0-no-copyleft-exception, LGPL-2.1, MIT, Apache-2.0
  1. /* This Source Code Form is subject to the terms of the Mozilla Public
  2. * License, v. 2.0. If a copy of the MPL was not distributed with this
  3. * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
  4. define([
  5. 'underscore',
  6. 'backbone',
  7. 'models/postcollection',
  8. 'models/stackcollection',
  9. 'models/livecollection',
  10. 'models/thumbnails',
  11. 'models/basemodel',
  12. 'lib/objecttools',
  13. 'lib/lazily',
  14. 'lib/promise',
  15. 'lib/errors'
  16. ],
  17. function (
  18. util,
  19. Backbone,
  20. PostCollection,
  21. StackCollection,
  22. LiveCollection,
  23. thumbnails,
  24. BaseModel,
  25. objectTools,
  26. lazily,
  27. Promise,
  28. errors
  29. ) {
  30. // A shared method used by augmented versions of `StackCollection`
  31. // and `PostCollection` below.
  32. var terms = function (terms) {
  33. if (terms) {
  34. this._terms = terms;
  35. // Update internal URL.`
  36. this.url(null, { q: terms });
  37. }
  38. this.state('invalid');
  39. return this._terms;
  40. };
  41. // Bing gets a custom update handler that doesn't diff (because search
  42. // results don't have IDs).
  43. var updateAndReset = function (terms) {
  44. // Update terms -- this should happen every time.
  45. this.terms(terms);
  46. // Diff the results of this collection when it comes back.
  47. return this.fetchDebounced();
  48. };
  49. // Alternative to underscore's debounce, this also creates and returns a promise
  50. function promisedDebounce(func, wait) {
  51. var timeout;
  52. return function() {
  53. var context = this, args = arguments;
  54. // eventualValue promise represents eventual outcome of func.apply
  55. // this is what we return and resolve
  56. var eventualValue = new Promise();
  57. var funcPromise = null;
  58. // later is called after our wait period
  59. // and effectively cancels previous calls
  60. var later = function() {
  61. timeout = null;
  62. // if there's a call already in-flight, cancel it
  63. if(funcPromise && funcPromise.cancel){
  64. // cancelling in-flight;
  65. funcPromise.cancel();
  66. funcPromise = null;
  67. }
  68. // make a promise for each call to func.
  69. // (to get a canceller, we have to have a promise to resolve a promise)
  70. funcPromise = new Promise(function(){
  71. // canceller
  72. // return a special Error so we can treat cancelling differently (its not really an error)
  73. return new errors.CancelledError();
  74. });
  75. // add callbacks for this call of func
  76. funcPromise.then(
  77. // success, we resolve the eventualValue promise we returned
  78. function(){
  79. // console.log("func call was successful, resolving eventualValue with: ", arguments[0]);
  80. eventualValue.resolve.apply(eventualValue, arguments);
  81. },
  82. function(err){
  83. // "failure", might be this call was cancelled
  84. if(err instanceof errors.CancelledError){
  85. // not an error, do nothing
  86. } else {
  87. // legit failure, reject the eventualValue
  88. eventualValue.reject.apply(eventualValue, arguments);
  89. }
  90. }
  91. );
  92. // trigger call to func. the outcome will result in funcPromise being either resolved or rejected
  93. // and providing we're not cancelled first, will resolve the eventualValue
  94. Promise.when(
  95. func.apply(context, args),
  96. function(){
  97. funcPromise.resolve.apply(funcPromise, arguments);
  98. funcPromise = null;
  99. },
  100. function(err){
  101. funcPromise.reject.apply(funcPromise, arguments);
  102. funcPromise = null;
  103. }
  104. );
  105. };
  106. clearTimeout(timeout);
  107. timeout = setTimeout(later, wait);
  108. return eventualValue;
  109. };
  110. }
  111. // Define a specialized `StackCollection` that gives us a few affordances
  112. // for dealing with searches.
  113. var __StackCollection = StackCollection.prototype;
  114. var StackSearchCollection = StackCollection.extend({
  115. terms: terms,
  116. // Create a debounced fetch method intented to be called from the
  117. // controller's `update` method as many times per second as you like.
  118. //
  119. // TODO: `_.debounce` doesn't return a value. If in future we find
  120. // it more useful to return a promise for the fetch, we should change
  121. // the way fetchDebounce is defined, or submit a patch to Underscore.js.
  122. fetchDebounced: function () {
  123. // Create the debounced function.
  124. var fetchDebounced = promisedDebounce(__StackCollection.fetch, 50);
  125. // Overshadow this function by setting the debounced function on the
  126. // object. Doing this means only one debounced function will be created
  127. // per instance.
  128. this.fetchDebounced = fetchDebounced;
  129. // ...and invoke this debounced function. Subsequent hits to
  130. // `fetchDebounced` will hit our debounced function directly.
  131. return fetchDebounced.apply(this, arguments);
  132. },
  133. // Define a custom parse method that adds the search terms
  134. // to each model. This property is **required** for
  135. // `PUT /lattice/:username/stack/search` calls.
  136. parseMatch: function (place) {
  137. place = __StackCollection.parseMatch(place);
  138. return util.extend(place, { search_terms: this.terms() });
  139. },
  140. // This method should be called from the controller's `update` method.
  141. // It is indended to be safe to call many times per second (e.g. it handles
  142. // debouncing fetches, etc).
  143. update: function (terms) {
  144. // Update terms -- this should happen every time.
  145. this.terms(terms);
  146. this.state('invalid');
  147. // Diff the results of this collection when it comes back.
  148. return this.fetchDebounced({ diff: true });
  149. }
  150. });
  151. // A shared method for Bing and Twitter searches. Added to the prototpe below.
  152. var fetchDebounced1000 = function () {
  153. // Create the debounced function.
  154. var fetchDebounced = promisedDebounce(__StackCollection.fetch, 1000);
  155. // Overshadow this function by setting the debounced function on the
  156. // object. Doing this means only one debounced function will be created
  157. // per instance.
  158. this.fetchDebounced = fetchDebounced;
  159. // ...and invoke this debounced function. Subsequent hits to
  160. // `fetchDebounced` will hit our debounced function directly.
  161. return fetchDebounced.apply(this, arguments);
  162. };
  163. // Define a special collection for collections. It is only allowed to have
  164. // up to 5 items.
  165. var __LiveCollection = LiveCollection.prototype;
  166. var SuggestionCollection = LiveCollection.extend({
  167. maxlength: 5,
  168. add: function (models, options) {
  169. var maxlength = this.maxlength;
  170. // Convert to array, if not array.
  171. if (!util.isArray(models)) models = [models];
  172. if (models.length > maxlength)
  173. models.splice(0, models.length - maxlength);
  174. var togetherLength = models.length + this.length;
  175. if (togetherLength > maxlength) {
  176. var difference = togetherLength - maxlength;
  177. // Remove items from the end of the stack. Since the Backbone.js
  178. // version we use now does not implement `pop()`, we use reduce
  179. // to accomplish the same thing.
  180. //
  181. // TODO: replace with `Collection pop()` when we upgrade Backbone.
  182. this.reduceRight(function (removed, model) {
  183. if (removed < difference) this.remove(model, options);
  184. return removed + 1;
  185. }, 0, this);
  186. }
  187. __LiveCollection.add.call(this, models, options);
  188. },
  189. parse: function (resp) { return resp.related || []; }
  190. });
  191. var __PostCollection = PostCollection.prototype;
  192. // BingSearchCollection is an ordinary mild-mannered PostCollection, but
  193. // has a terms method and a debounced fetch method.
  194. var BingSearchCollection = PostCollection.extend({
  195. initialize: function (models, options) {
  196. __PostCollection.initialize.call(this, models, options);
  197. this.bind('success', this.onSuccess, this);
  198. },
  199. // Default handler for processing images and suggestions.
  200. onSuccess: function (resp, options) {
  201. thumbnails.parse.call(this, resp);
  202. this.images().process(resp, options);
  203. this.suggestions().process(resp, options);
  204. },
  205. terms: terms,
  206. // Create a debounced fetch method intented to be called from the
  207. // controller's `update` method as many times per second as you like.
  208. fetchDebounced: fetchDebounced1000,
  209. update: updateAndReset,
  210. images: BaseModel.subcollection('images', function () {
  211. var collection = new LiveCollection();
  212. // Create a custom parse function for this instance.
  213. collection.parse = function (resp) { return resp.images || []; };
  214. return collection;
  215. }),
  216. suggestions: BaseModel.subcollection('suggestions', function () {
  217. return new SuggestionCollection();
  218. })
  219. });
  220. var TwitterSearchCollection = PostCollection.extend({
  221. terms: terms,
  222. // Create a debounced fetch method intented to be called from the
  223. // controller's `update` method as many times per second as you like.
  224. fetchDebounced: fetchDebounced1000,
  225. update: updateAndReset,
  226. // `url: ...` inherited from SiteCollection. Wraps `this.UrlHelper.url`.
  227. // A custom parse method for Twitter results.
  228. // Twitter results look like this:
  229. //
  230. // {
  231. // "popular": [
  232. // {
  233. // "friend_name": "...",
  234. // "friend_username": "...",
  235. // "summary": "...",
  236. // "friend_img": "...",
  237. // "time": "Thu, 03 May 2012 18:51:34 +0000",
  238. // "id": 198122645315788800
  239. // }
  240. // ],
  241. // "recent": [
  242. // {
  243. // "friend_name": "...",
  244. // "friend_username": "...",
  245. // "summary": "...",
  246. // "friend_img": "...",
  247. // "time": "Thu, 03 May 2012 18:51:34 +0000",
  248. // "id": 198122645315788800
  249. // }
  250. // ]
  251. parse: function (resp) {
  252. var results = resp.recent;
  253. // nothing to do here
  254. if(!results.length) return [];
  255. // Translate response array using curried translator function.
  256. results = util.map(results, function (tweet) {
  257. var urls = tweet.urls;
  258. // Grab the first URL in the urls array. We'll use that.
  259. tweet.url = (urls && urls.length > 0) ? urls[0] : '';
  260. return this.translateKeys(tweet);
  261. }, this);
  262. return results;
  263. },
  264. translateKeys: util.bind(objectTools.translate, null, {
  265. 'id': 'place_id',
  266. 'title': 'place_title',
  267. 'url': 'place_url',
  268. 'friend_img': 'avatar',
  269. // Results may vary re: author vs friend_name. We're passing back `author`
  270. // for news via results.
  271. 'author': 'author',
  272. 'friend_name': 'author',
  273. 'friend_username': 'username',
  274. // The search API returns time as an absolute value under this key.
  275. 'time': 'published',
  276. 'summary': 'summary'
  277. })
  278. });
  279. var prepareUrlModelFromUrl = function(url) {
  280. return { 'place_url': url.url || url, 'place_title': url.title || url };
  281. };
  282. var UrlCollection = LiveCollection.extend({
  283. model: Backbone.Model.extend({
  284. // UrlModels expect a single URL string value as input
  285. // ..though maybe we want to track the matched string and extrapolated url as 2 properties?
  286. parse: prepareUrlModelFromUrl
  287. })
  288. });
  289. // install static helpers
  290. UrlCollection.prepareModelFromUrl = prepareUrlModelFromUrl;
  291. // Export modules. We may as well keep the names short and sweet, since it
  292. // will be clear what they are from their reference object
  293. // (`searchCollections.Bing`, etc).
  294. return {
  295. Url: UrlCollection,
  296. Stack: StackSearchCollection,
  297. Bing: BingSearchCollection,
  298. Tweets: TwitterSearchCollection
  299. };
  300. });