wip metadata
[myslice.git] / manifold / js / manifold.js
1 // utilities 
2 function debug_dict_keys (msg, o) {
3     var keys=[];
4     for (var k in o) keys.push(k);
5     messages.debug ("debug_dict_keys: " + msg + " keys= " + keys);
6 }
7 function debug_dict (msg, o) {
8     for (var k in o) messages.debug ("debug_dict: " + msg + " [" + k + "]=" + o[k]);
9 }
10 function debug_value (msg, value) {
11     messages.debug ("debug_value: " + msg + " " + value);
12 }
13 function debug_query (msg, query) {
14     if (query === undefined) messages.debug ("debug_query: " + msg + " -> undefined");
15     else if (query == null) messages.debug ("debug_query: " + msg + " -> null");
16     else if ('query_uuid' in query) messages.debug ("debug_query: " + msg + query.__repr());
17     else messages.debug ("debug_query: " + msg + " query= " + query);
18 }
19
20 /* ------------------------------------------------------------ */
21
22 // Constants that should be somehow moved to a plugin.js file
23 var FILTER_ADDED   = 1;
24 var FILTER_REMOVED = 2;
25 var CLEAR_FILTERS  = 3;
26 var FIELD_ADDED    = 4;
27 var FIELD_REMOVED  = 5;
28 var CLEAR_FIELDS   = 6;
29 var NEW_RECORD     = 7;
30 var CLEAR_RECORDS  = 8;
31
32 var IN_PROGRESS    = 101;
33 var DONE           = 102;
34
35 var SET_ADD        = 201;
36 var SET_REMOVED    = 202;
37
38 // A structure for storing queries
39
40 function QueryExt(query, parent_query, main_query) {
41
42     /* Constructor */
43     if (typeof query == "undefined")
44         throw "Must pass a query in QueryExt constructor";
45     this.query        = query
46     this.parent_query = (typeof parent_query == "undefined") ? false : parent_query
47     this.main_query   = (typeof main_query   == "undefined") ? false : main_query
48
49     // How to link to an update query ? they both have the same UUID !!
50
51 }
52
53 function QueryStore() {
54
55     var main_queries     = {};
56     var analyzed_queries = {};
57
58     /* Insertion */
59
60     this.insert = function(query)
61     {
62         if (query.analyzed_query == null) {
63             query.analyze_subqueries();
64         }
65
66         query_ext = QueryExt(query, null, null)
67         manifold.query_store.main_queries[query.query_uuid] = query_ext;
68
69         // We also need to insert all queries and subqueries from the analyzed_query
70         query.iter_subqueries(function(sq, data, parent_query) {
71             parent_query_ext = this.find_analyzed_query_ext(parent_query.query_uuid);
72             sq_ext = QueryExt(sq, parent_query_ext, query_ext)
73             this.analyzed_queries[sq.query_uuid] = sq_ext;
74         });
75     }
76
77     /* Searching */
78
79     this.find_query_ext = function(query_uuid)
80     {
81         return this.main_queries[query_uuid];
82     }
83
84     this.find_query = function(query_uuid)
85     {
86         return this.find_query_ext(query_uuid).query;
87     }
88
89     this.find_analyzed_query_ext = function(query_uuid)
90     {
91         return this.analyzed_queries[query_uuid];
92     }
93
94     this.find_analyzed_query = function(query_uuid)
95     {
96         return this.find_analyzed_query_ext(query_uuid).query;
97     }
98 }
99
100 /*!
101  * This namespace holds functions for globally managing query objects
102  * \Class Manifold
103  */
104 var manifold = {
105
106     /************************************************************************** 
107      * Helper functions
108      **************************************************************************/ 
109
110     spin_presets: {},
111
112     spin: function(locator, active /*= true */) {
113         active = typeof active !== 'undefined' ? active : true;
114         try {
115             if (active) {
116                 $(locator).spin(manifold.spin_presets);
117             } else {
118                 $(locator).spin(false);
119             }
120         } catch (err) { messages.debug("Cannot turn spins on/off " + err); }
121     },
122
123     /************************************************************************** 
124      * Query management
125      **************************************************************************/ 
126
127     query_store: QueryStore(),
128
129     // XXX Remaining functions are deprecated since they are replaced by the query store
130
131     /*!
132      * Associative array storing the set of queries active on the page
133      * \memberof Manifold
134      */
135     all_queries: {},
136
137     /*!
138      * Insert a query in the global hash table associating uuids to queries.
139      * If the query has no been analyzed yet, let's do it.
140      * \fn insert_query(query)
141      * \memberof Manifold
142      * \param ManifoldQuery query Query to be added
143      */
144     insert_query : function (query) { 
145         if (query.analyzed_query == null) {
146             query.analyze_subqueries();
147         }
148         manifold.all_queries[query.query_uuid]=query;
149     },
150
151     /*!
152      * Returns the query associated to a UUID
153      * \fn find_query(query_uuid)
154      * \memberof Manifold
155      * \param string query_uuid The UUID of the query to be returned
156      */
157     find_query : function (query_uuid) { 
158         return manifold.all_queries[query_uuid];
159     },
160
161     /************************************************************************** 
162      * Query execution
163      **************************************************************************/ 
164
165     // trigger a query asynchroneously
166     proxy_url : '/manifold/proxy/json/',
167
168     asynchroneous_debug : true,
169
170     /**
171      * \brief We use js function closure to be able to pass the query (array)
172      * to the callback function used when data is received
173      */
174     success_closure: function(query, publish_uuid, callback /*domid*/)
175     {
176         return function(data, textStatus) {
177             manifold.asynchroneous_success(data, query, publish_uuid, callback /*domid*/);
178         }
179     },
180
181     // Executes all async. queries
182     // input queries are specified as a list of {'query_uuid': <query_uuid>, 'id': <possibly null>}
183     asynchroneous_exec : function (query_publish_dom_tuples) {
184         // start spinners
185
186         // in case the spin stuff was not loaded, let's make sure we proceed to the exit 
187         //try {
188         //    if (manifold.asynchroneous_debug) 
189         //   messages.debug("Turning on spin with " + jQuery(".need-spin").length + " matches for .need-spin");
190         //    jQuery('.need-spin').spin(manifold.spin_presets);
191         //} catch (err) { messages.debug("Cannot turn on spins " + err); }
192         
193         // Loop through input array, and use publish_uuid to publish back results
194         jQuery.each(query_publish_dom_tuples, function(index, tuple) {
195             var query=manifold.find_query(tuple.query_uuid);
196             var query_json=JSON.stringify (query);
197             var publish_uuid=tuple.publish_uuid;
198             // by default we publish using the same uuid of course
199             if (publish_uuid==undefined) publish_uuid=query.query_uuid;
200             if (manifold.asynchroneous_debug) {
201                 messages.debug("sending POST on " + manifold.proxy_url + " to be published on " + publish_uuid);
202                 messages.debug("... ctd... with actual query= " + query.__repr());
203             }
204
205             query.iter_subqueries(function (sq) {
206                 manifold.raise_record_event(sq.query_uuid, IN_PROGRESS);
207             });
208
209             // not quite sure what happens if we send a string directly, as POST data is named..
210             // this gets reconstructed on the proxy side with ManifoldQuery.fill_from_POST
211                 jQuery.post(manifold.proxy_url, {'json':query_json} , manifold.success_closure(query, publish_uuid, tuple.callback /*domid*/));
212         })
213     },
214
215     /**
216      * \brief Forward a query to the manifold backend
217      * \param query (dict) the query to be executed asynchronously
218      * \param callback (function) the function to be called when the query terminates
219      * Deprecated:
220      * \param domid (string) the domid to be notified about the results (null for using the pub/sub system
221      */
222     forward: function(query, callback /*domid*/) {
223         var query_json = JSON.stringify(query);
224         $.post(manifold.proxy_url, {'json': query_json} , manifold.success_closure(query, query.query_uuid, callback/*domid*/));
225     },
226
227     /*!
228      * Returns whether a query expects a unique results.
229      * This is the case when the filters contain a key of the object
230      * \fn query_expects_unique_result(query)
231      * \memberof Manifold
232      * \param ManifoldQuery query Query for which we are testing whether it expects a unique result
233      */
234     query_expects_unique_result: function(query) {
235         /* XXX we need functions to query metadata */
236         //var keys = MANIFOLD_METADATA[query.object]['keys']; /* array of array of field names */
237         /* TODO requires keys in metadata */
238         return true;
239     },
240
241     /*!
242      * Publish result
243      * \fn publish_result(query, results)
244      * \memberof Manifold
245      * \param ManifoldQuery query Query which has received results
246      * \param array results results corresponding to query
247      */
248     publish_result: function(query, result) {
249         if (typeof result === 'undefined')
250             result = [];
251
252         // NEW PLUGIN API
253         manifold.raise_record_event(query.query_uuid, CLEAR_RECORDS);
254         $.each(result, function(i, record) {
255             manifold.raise_record_event(query.query_uuid, NEW_RECORD, record);
256         });
257         manifold.raise_record_event(query.query_uuid, DONE);
258
259         // OLD PLUGIN API BELOW
260         /* Publish an update announce */
261         var channel="/results/" + query.query_uuid + "/changed";
262         if (manifold.asynchroneous_debug)
263             messages.debug("publishing result on " + channel);
264         jQuery.publish(channel, [result, query]);
265     },
266
267     /*!
268      * Recursively publish result
269      * \fn publish_result_rec(query, result)
270      * \memberof Manifold
271      * \param ManifoldQuery query Query which has received result
272      * \param array result result corresponding to query
273      */
274     publish_result_rec: function(query, result) {
275         /* If the result is not unique, only publish the top query;
276          * otherwise, publish the main object as well as subqueries
277          * XXX how much recursive are we ?
278          */
279         if (manifold.query_expects_unique_result(query)) {
280             /* Also publish subqueries */
281             jQuery.each(query.subqueries, function(object, subquery) {
282                 manifold.publish_result_rec(subquery, result[0][object]);
283                 /* TODO remove object from result */
284             });
285         }
286         manifold.publish_result(query, result);
287     },
288
289     // if set domid allows the result to be directed to just one plugin
290     // most of the time publish_uuid will be query.query_uuid
291     // however in some cases we wish to publish the result under a different uuid
292     // e.g. an updater wants to publish its result as if from the original (get) query
293     asynchroneous_success : function (data, query, publish_uuid, callback /*domid*/) {
294         // xxx should have a nicer declaration of that enum in sync with the python code somehow
295
296         /* If a callback has been specified, we redirect results to it */
297         if (!!callback) { callback(data); return; }
298
299         if (data.code == 2) { // ERROR
300             // We need to make sense of error codes here
301             alert("Your session has expired, please log in again");
302             window.location="/logout/";
303             return;
304         }
305         if (data.code == 1) { // WARNING
306             messages.error("Some errors have been received from the manifold backend at " + MANIFOLD_URL + " [" + data.description + "]");
307             // publish error code and text message on a separate channel for whoever is interested
308             jQuery.publish("/results/" + publish_uuid + "/failed", [data.code, data.description] );
309
310             $("#notifications").notify("create", "sticky", {
311               title: 'Warning',
312               text: data.description
313             },{
314               expires: false,
315               speed: 1000
316             });
317             
318         }
319         // once everything is checked we can use the 'value' part of the manifoldresult
320         var result=data.value;
321         if (result) {
322             //if (!!callback /* domid */) {
323             //    /* Directly inform the requestor */
324             //    if (manifold.asynchroneous_debug) messages.debug("directing result to callback");
325             //    callback(result);
326             //    //if (manifold.asynchroneous_debug) messages.debug("directing result to " + domid);
327             //    //jQuery('#' + domid).trigger('results', [result]);
328             //} else {
329                 /* XXX Jordan XXX I don't need publish_uuid here... What is it used for ? */
330                 /* query is the query we sent to the backend; we need to find the
331                  * corresponding analyezd_query in manifold.all_queries
332                  */
333                 tmp_query = manifold.find_query(query.query_uuid);
334                 manifold.publish_result_rec(tmp_query.analyzed_query, result);
335             //}
336
337         }
338     },
339
340     /************************************************************************** 
341      * Plugin API helpers
342      **************************************************************************/ 
343
344     raise_event_handler: function(type, query_uuid, event_type, value)
345     {
346         if (type == 'query') {
347             var channels = [ manifold.get_query_channel(query_uuid), manifold.get_query_channel('*') ];
348         } else if (type == 'record') {
349             var channels = [ manifold.get_record_channel(query_uuid), manifold.get_record_channel('*') ];
350
351         } else {
352             throw 'Incorrect type for manifold.raise_event()';
353         }
354         $.each(channels, function(i, channel) {
355             if (value === undefined)
356                 $('.plugin').trigger(channel, [event_type]);
357             else
358                 $('.plugin').trigger(channel, [event_type, value]);
359         });
360     },
361
362     raise_query_event: function(query_uuid, event_type, value)
363     {
364         manifold.raise_event_handler('query', query_uuid, event_type, value);
365     },
366
367     raise_record_event: function(query_uuid, event_type, value)
368     {
369         manifold.raise_event_handler('record', query_uuid, event_type, value);
370     },
371
372
373     raise_event: function(query_uuid, event_type, value)
374     {
375         switch(event_type) {
376             case SET_ADD:
377                 // Query uuid has been updated with the key of a new element
378                 query = manifold.find_query(query_uuid);
379
380                 // XXX We need to find the parent to update the property
381                 // XXX We need to find the non-analyzed query so that it can be updated also
382                 break;
383             case SET_REMOVED:
384                 // Query uuid has been updated with the key of a removed element
385                 break;
386         }
387     },
388
389     /* Publish/subscribe channels for internal use */
390     get_query_channel:  function(uuid) { return '/query/'  + uuid },
391     get_record_channel: function(uuid) { return '/record/' + uuid },
392
393 }; // manifold object
394 /* ------------------------------------------------------------ */
395
396 (function($) {
397
398     /* NEW PLUGIN API
399      * 
400      * NOTE: Since we don't have a plugin class, we are extending all jQuery
401      * plugins...
402      */
403
404     /*!
405      * \brief Associates a query handler to the current plugin
406      * \param uuid (string) query uuid
407      * \param handler (function) handler callback
408      */
409     $.fn.set_query_handler = function(uuid, handler)
410     {
411         this.on(manifold.get_query_channel(uuid), handler);
412     }
413
414     $.fn.set_record_handler = function(uuid, handler)
415     {
416         this.on(manifold.get_record_channel(uuid), handler);
417     }
418
419     // OLD PLUGIN API: extend jQuery/$ with pubsub capabilities
420     // https://gist.github.com/661855
421     var o = $({});
422     $.subscribe = function( channel, selector, data, fn) {
423       /* borrowed from jQuery */
424       if ( data == null && fn == null ) {
425           // ( channel, fn )
426           fn = selector;
427           data = selector = undefined;
428       } else if ( fn == null ) {
429           if ( typeof selector === "string" ) {
430               // ( channel, selector, fn )
431               fn = data;
432               data = undefined;
433           } else {
434               // ( channel, data, fn )
435               fn = data;
436               data = selector;
437               selector = undefined;
438           }
439       }
440       /* </ugly> */
441   
442       /* We use an indirection function that will clone the object passed in
443        * parameter to the subscribe callback 
444        * 
445        * FIXME currently we only clone query objects which are the only ones
446        * supported and editable, we might have the same issue with results but
447        * the page load time will be severely affected...
448        */
449       o.on.apply(o, [channel, selector, data, function() { 
450           for(i = 1; i < arguments.length; i++) {
451               if ( arguments[i].constructor.name == 'ManifoldQuery' )
452                   arguments[i] = arguments[i].clone();
453           }
454           fn.apply(o, arguments);
455       }]);
456     };
457   
458     $.unsubscribe = function() {
459       o.off.apply(o, arguments);
460     };
461   
462     $.publish = function() {
463       o.trigger.apply(o, arguments);
464     };
465   
466 }(jQuery));
467
468 /* ------------------------------------------------------------ */
469
470 //http://stackoverflow.com/questions/5100539/django-csrf-check-failing-with-an-ajax-post-request
471 //make sure to expose csrf in our outcoming ajax/post requests
472 $.ajaxSetup({ 
473      beforeSend: function(xhr, settings) {
474          function getCookie(name) {
475              var cookieValue = null;
476              if (document.cookie && document.cookie != '') {
477                  var cookies = document.cookie.split(';');
478                  for (var i = 0; i < cookies.length; i++) {
479                      var cookie = jQuery.trim(cookies[i]);
480                      // Does this cookie string begin with the name we want?
481                  if (cookie.substring(0, name.length + 1) == (name + '=')) {
482                      cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
483                      break;
484                  }
485              }
486          }
487          return cookieValue;
488          }
489          if (!(/^http:.*/.test(settings.url) || /^https:.*/.test(settings.url))) {
490              // Only send the token to relative URLs i.e. locally.
491              xhr.setRequestHeader("X-CSRFToken", getCookie('csrftoken'));
492          }
493      } 
494 });
495