1"use strict";
2/* All shared functionality to go in libtoaster object.
3 * This object really just helps readability since we can then have
4 * a traceable namespace.
5 */
6var libtoaster = (function () {
7  // prevent conflicts with Bootstrap 2's typeahead (required during
8  // transition from v2 to v3)
9  var typeahead = jQuery.fn.typeahead.noConflict();
10  jQuery.fn._typeahead = typeahead;
11
12  /* Make a typeahead from an input element
13   *
14   * _makeTypeahead parameters
15   * jQElement: input element as selected by $('selector')
16   * xhrUrl: the url to get the JSON from; this URL should return JSON in the
17   * format:
18   *   { "results": [ { "name": "test", "detail" : "a test thing"  }, ... ] }
19   * xhrParams: the data/parameters to pass to the getJSON url e.g.
20   *   { 'type' : 'projects' }; the text typed will be passed as 'search'.
21   * selectedCB: function to call once an item has been selected; has
22   * signature selectedCB(item), where item is an item in the format shown
23   * in the JSON list above, i.e.
24   *   { "name": "name", "detail": "detail" }.
25   */
26  function _makeTypeahead(jQElement, xhrUrl, xhrParams, selectedCB) {
27    if (!xhrUrl || xhrUrl.length === 0) {
28      throw("No url supplied for typeahead");
29    }
30
31    var xhrReq;
32
33    jQElement._typeahead(
34      {
35        highlight: true,
36        classNames: {
37          open: "dropdown-menu",
38          cursor: "active"
39        }
40      },
41      {
42        source: function (query, syncResults, asyncResults) {
43          xhrParams.search = query;
44
45          // if we have a request in progress, cancel it and start another
46          if (xhrReq) {
47            xhrReq.abort();
48          }
49
50          xhrReq = $.getJSON(xhrUrl, xhrParams, function (data) {
51            if (data.error !== "ok") {
52              console.error("Error getting data from server: " + data.error);
53              return;
54            }
55
56            xhrReq = null;
57
58            asyncResults(data.results);
59          });
60        },
61
62        // how the selected item is shown in the input
63        display: function (item) {
64          return item.name;
65        },
66
67        templates: {
68          // how the item is displayed in the dropdown
69          suggestion: function (item) {
70            var elt = document.createElement("div");
71            elt.innerHTML = item.name + " " + item.detail;
72            return elt;
73          }
74        }
75      }
76    );
77
78    // when an item is selected using the typeahead, invoke the callback
79    jQElement.on("typeahead:select", function (event, item) {
80      selectedCB(item);
81    });
82  }
83
84  /* startABuild:
85   * url: xhr_buildrequest or null for current project
86   * targets: an array or space separated list of targets to build
87   * onsuccess: callback for successful execution
88   * onfail: callback for failed execution
89   */
90  function _startABuild (url, targets, onsuccess, onfail) {
91
92    if (!url)
93      url = libtoaster.ctx.xhrBuildRequestUrl;
94
95    /* Flatten the array of targets into a space spearated list */
96    if (targets instanceof Array){
97      targets = targets.reduce(function(prevV, nextV){
98        return prev + ' ' + next;
99      });
100    }
101
102    $.ajax( {
103        type: "POST",
104        url: url,
105        data: { 'targets' : targets },
106        headers: { 'X-CSRFToken' : $.cookie('csrftoken')},
107        success: function (_data) {
108          if (_data.error !== "ok") {
109            console.warn(_data.error);
110          } else {
111            if (onsuccess !== undefined) onsuccess(_data);
112          }
113        },
114        error: function (_data) {
115          console.warn("Call failed");
116          console.warn(_data);
117          if (onfail) onfail(data);
118    } });
119  }
120
121  /* cancelABuild:
122   * url: xhr_buildrequest url or null for current project
123   * buildRequestIds: space separated list of build request ids
124   * onsuccess: callback for successful execution
125   * onfail: callback for failed execution
126   */
127  function _cancelABuild(url, buildRequestIds, onsuccess, onfail){
128    if (!url)
129      url = libtoaster.ctx.xhrBuildRequestUrl;
130
131    $.ajax( {
132        type: "POST",
133        url: url,
134        data: { 'buildCancel': buildRequestIds },
135        headers: { 'X-CSRFToken' : $.cookie('csrftoken')},
136        success: function (_data) {
137          if (_data.error !== "ok") {
138            console.warn(_data.error);
139          } else {
140            if (onsuccess) onsuccess(_data);
141          }
142        },
143        error: function (_data) {
144          console.warn("Call failed");
145          console.warn(_data);
146          if (onfail) onfail(_data);
147        }
148    });
149  }
150
151  function _getMostRecentBuilds(url, onsuccess, onfail) {
152    $.ajax({
153      url: url,
154      type: 'GET',
155      data : {format: 'json'},
156      headers: {'X-CSRFToken': $.cookie('csrftoken')},
157      success: function (data) {
158        onsuccess ? onsuccess(data) : console.log(data);
159      },
160      error: function (data) {
161        onfail ? onfail(data) : console.error(data);
162      }
163    });
164  }
165
166  /* Get a project's configuration info */
167  function _getProjectInfo(url, onsuccess, onfail){
168    $.ajax({
169        type: "GET",
170        url: url,
171        headers: { 'X-CSRFToken' : $.cookie('csrftoken')},
172        success: function (_data) {
173          if (_data.error !== "ok") {
174            console.warn(_data.error);
175          } else {
176            if (onsuccess !== undefined) onsuccess(_data);
177          }
178        },
179        error: function (_data) {
180          console.warn(_data);
181          if (onfail) onfail(_data);
182        }
183    });
184  }
185
186  /* Properties for data can be:
187   * layerDel (csv)
188   * layerAdd (csv)
189   * projectName
190   * projectVersion
191   * machineName
192   */
193  function _editCurrentProject(data, onSuccess, onFail){
194    $.ajax({
195        type: "POST",
196        url: libtoaster.ctx.xhrProjectUrl,
197        data: data,
198        headers: { 'X-CSRFToken' : $.cookie('csrftoken')},
199        success: function (data) {
200          if (data.error != "ok") {
201            console.log(data.error);
202            if (onFail !== undefined)
203              onFail(data);
204          } else {
205            if (onSuccess !== undefined)
206              onSuccess(data);
207          }
208        },
209        error: function (data) {
210          console.log("Call failed");
211          console.log(data);
212        }
213    });
214  }
215
216  function _getLayerDepsForProject(url, onSuccess, onFail){
217    /* Check for dependencies not in the current project */
218    $.getJSON(url,
219      { format: 'json' },
220      function(data) {
221        if (data.error != "ok") {
222          console.log(data.error);
223          if (onFail !== undefined)
224            onFail(data);
225        } else {
226          var deps = {};
227          /* Filter out layer dep ids which are in the
228           * project already.
229           */
230          deps.list = data.layerdeps.list.filter(function(layerObj){
231            return (data.projectlayers.lastIndexOf(layerObj.id) < 0);
232          });
233
234          onSuccess(deps);
235        }
236      }, function() {
237        console.log("E: Failed to make request");
238    });
239  }
240
241  /* parses the query string of the current window.location to an object */
242  function _parseUrlParams() {
243    var string = window.location.search;
244    string = string.substr(1);
245    var stringArray = string.split ("&");
246    var obj = {};
247
248    for (var i in stringArray) {
249      var keyVal = stringArray[i].split ("=");
250      obj[keyVal[0]] = keyVal[1];
251    }
252
253    return obj;
254  }
255
256  /* takes a flat object and outputs it as a query string
257   * e.g. the output of dumpsUrlParams
258   */
259  function _dumpsUrlParams(obj) {
260    var str = "?";
261
262    for (var key in obj){
263      if (!obj[key])
264        continue;
265
266      str += key+ "="+obj[key].toString();
267      str += "&";
268    }
269
270    /* Maintain the current hash */
271    str += window.location.hash;
272
273    return str;
274  }
275
276  function _addRmLayer(layerObj, add, doneCb){
277    if (layerObj.xhrLayerUrl === undefined){
278      alert("ERROR: missing xhrLayerUrl object. Please file a bug.");
279      return;
280    }
281
282    if (add === true) {
283      /* If adding get the deps for this layer */
284      libtoaster.getLayerDepsForProject(layerObj.xhrLayerUrl,
285        function (layers) {
286
287        /* got result for dependencies */
288        if (layers.list.length === 0){
289          var editData = { layerAdd : layerObj.id };
290          libtoaster.editCurrentProject(editData, function() {
291            doneCb([]);
292          });
293          return;
294        } else {
295          try {
296            showLayerDepsModal(layerObj, layers.list, null, null,  true, doneCb);
297          }  catch (e) {
298            $.getScript(libtoaster.ctx.jsUrl + "layerDepsModal.js", function(){
299              showLayerDepsModal(layerObj, layers.list, null, null,  true, doneCb);
300            }, function(){
301              console.warn("Failed to load layerDepsModal");
302            });
303          }
304        }
305      }, null);
306    } else if (add === false) {
307      var editData = { layerDel : layerObj.id };
308
309      libtoaster.editCurrentProject(editData, function () {
310        doneCb([]);
311      }, function () {
312        console.warn ("Removing layer from project failed");
313        doneCb(null);
314      });
315    }
316  }
317
318  function _makeLayerAddRmAlertMsg(layer, layerDepsList, add) {
319    var alertMsg;
320
321    if (layerDepsList.length > 0 && add === true) {
322      alertMsg = $("<span>You have added <strong>"+(layerDepsList.length+1)+"</strong> layers to your project: <a class=\"alert-link\" id=\"layer-affected-name\"></a> and its dependencies </span>");
323
324      /* Build the layer deps list */
325      layerDepsList.map(function(layer, i){
326        var link = $("<a class=\"alert-link\"></a>");
327
328        link.attr("href", layer.layerdetailurl);
329        link.text(layer.name);
330        link.tooltip({title: layer.tooltip});
331
332        if (i !== 0)
333          alertMsg.append(", ");
334
335        alertMsg.append(link);
336      });
337    } else if (layerDepsList.length === 0 && add === true) {
338      alertMsg = $("<span>You have added <strong>1</strong> layer to your project: <a class=\"alert-link\" id=\"layer-affected-name\"></a></span></span>");
339    } else if (add === false) {
340      alertMsg = $("<span>You have removed <strong>1</strong> layer from your project: <a class=\"alert-link\" id=\"layer-affected-name\"></a></span>");
341    }
342
343    alertMsg.children("#layer-affected-name").text(layer.name);
344    alertMsg.children("#layer-affected-name").attr("href", layer.layerdetailurl);
345
346    return alertMsg.html();
347  }
348
349  function _showChangeNotification(message){
350    $(".alert-dismissible").fadeOut().promise().done(function(){
351      var alertMsg = $("#change-notification-msg");
352
353      alertMsg.html(message);
354      $("#change-notification, #change-notification *").fadeIn();
355    });
356  }
357
358  function _createCustomRecipe(name, baseRecipeId, doneCb){
359    var data = {
360      'name' : name,
361      'project' : libtoaster.ctx.projectId,
362      'base' : baseRecipeId,
363    };
364
365    $.ajax({
366        type: "POST",
367        url: libtoaster.ctx.xhrCustomRecipeUrl,
368        data: data,
369        headers: { 'X-CSRFToken' : $.cookie('csrftoken')},
370        success: function (ret) {
371          if (doneCb){
372            doneCb(ret);
373          } else if (ret.error !== "ok") {
374            console.warn(ret.error);
375          }
376        },
377        error: function (ret) {
378          console.warn("Call failed");
379          console.warn(ret);
380        }
381    });
382  }
383
384  /* Validate project names. Use unique project names
385
386     All arguments accepted by this function are JQeury objects.
387
388     For example if the HTML element has "hint-error-project-name", then
389     it is passed to this function as $("#hint-error-project-name").
390
391     Arg1 - projectName : This is a string object. In the HTML, project name will be entered here.
392     Arg2 - hintEerror : This is a jquery object which will accept span which throws error for
393            duplicate project
394     Arg3 - ctrlGrpValidateProjectName : This object holds the div with class "control-group"
395     Arg4 - enableOrDisableBtn : This object will help the API to enable or disable the form.
396            For example in the new project the create project button will be hidden if the
397            duplicate project exist. Similarly in the projecttopbar the save button will be
398            disabled if the project name already exist.
399
400     Return - This function doesn't return anything. It sets/unsets the behavior of the elements.
401  */
402
403  function _makeProjectNameValidation(projectName, hintError,
404                ctrlGrpValidateProjectName, enableOrDisableBtn ) {
405
406     function checkProjectName(projectName){
407       $.ajax({
408            type: "GET",
409            url: libtoaster.ctx.projectsTypeAheadUrl,
410            data: { 'search' : projectName },
411            headers: { 'X-CSRFToken' : $.cookie('csrftoken')},
412            success: function(data){
413              if (data.results.length > 0 &&
414                  data.results[0].name === projectName) {
415                // This project name exists hence show the error and disable
416                // the save button
417                ctrlGrpValidateProjectName.addClass('has-error');
418                hintError.show();
419                enableOrDisableBtn.attr('disabled', 'disabled');
420              } else {
421                ctrlGrpValidateProjectName.removeClass('has-error');
422                hintError.hide();
423                enableOrDisableBtn.removeAttr('disabled');
424              }
425            },
426            error: function (data) {
427              console.log(data);
428            },
429       });
430     }
431
432     /* The moment user types project name remove the error */
433     projectName.on("input", function() {
434        var projectName = $(this).val();
435        checkProjectName(projectName)
436     });
437
438     /* Validate new project name */
439     projectName.on("blur", function(){
440        var projectName = $(this).val();
441        checkProjectName(projectName)
442     });
443  }
444
445  // if true, the loading spinner for Ajax requests will be displayed
446  // if requests take more than 1200ms
447  var ajaxLoadingTimerEnabled = true;
448
449  // turn on the page-level loading spinner for Ajax requests
450  function _enableAjaxLoadingTimer() {
451    ajaxLoadingTimerEnabled = true;
452  }
453
454  // turn off the page-level loading spinner for Ajax requests
455  function _disableAjaxLoadingTimer() {
456    ajaxLoadingTimerEnabled = false;
457  }
458
459  /* Utility function to set a notification for the next page load */
460  function _setNotification(name, message){
461    var data = {
462      name: name,
463      message: message
464    };
465
466    $.cookie('toaster-notification', JSON.stringify(data), { path: '/'});
467  }
468
469  /* _updateProject:
470   * url: xhrProjectUpdateUrl or null for current project
471   * onsuccess: callback for successful execution
472   * onfail: callback for failed execution
473   */
474  function _updateProject (url, targets, default_image, onsuccess, onfail) {
475
476    if (!url)
477      url = libtoaster.ctx.xhrProjectUpdateUrl;
478
479    /* Flatten the array of targets into a space spearated list */
480    if (targets instanceof Array){
481      targets = targets.reduce(function(prevV, nextV){
482        return prev + ' ' + next;
483      });
484    }
485
486    $.ajax( {
487        type: "POST",
488        url: url,
489        data: { 'do_update' : 'True' , 'targets' : targets , 'default_image' : default_image , },
490        headers: { 'X-CSRFToken' : $.cookie('csrftoken')},
491        success: function (_data) {
492          if (_data.error !== "ok") {
493            console.warn(_data.error);
494          } else {
495            if (onsuccess !== undefined) onsuccess(_data);
496          }
497        },
498        error: function (_data) {
499          console.warn("Call failed");
500          console.warn(_data);
501          if (onfail) onfail(data);
502    } });
503  }
504
505  /* _cancelProject:
506   * url: xhrProjectUpdateUrl or null for current project
507   * onsuccess: callback for successful execution
508   * onfail: callback for failed execution
509   */
510  function _cancelProject (url, onsuccess, onfail) {
511
512    if (!url)
513      url = libtoaster.ctx.xhrProjectCancelUrl;
514
515    $.ajax( {
516        type: "POST",
517        url: url,
518        data: { 'do_cancel' : 'True'  },
519        headers: { 'X-CSRFToken' : $.cookie('csrftoken')},
520        success: function (_data) {
521          if (_data.error !== "ok") {
522            console.warn(_data.error);
523          } else {
524            if (onsuccess !== undefined) onsuccess(_data);
525          }
526        },
527        error: function (_data) {
528          console.warn("Call failed");
529          console.warn(_data);
530          if (onfail) onfail(data);
531    } });
532  }
533
534  /* _setDefaultImage:
535   * url: xhrSetDefaultImageUrl or null for current project
536   * targets: an array or space separated list of targets to set as default
537   * onsuccess: callback for successful execution
538   * onfail: callback for failed execution
539   */
540  function _setDefaultImage (url, targets, onsuccess, onfail) {
541
542    if (!url)
543      url = libtoaster.ctx.xhrSetDefaultImageUrl;
544
545    /* Flatten the array of targets into a space spearated list */
546    if (targets instanceof Array){
547      targets = targets.reduce(function(prevV, nextV){
548        return prev + ' ' + next;
549      });
550    }
551
552    $.ajax( {
553        type: "POST",
554        url: url,
555        data: { 'targets' : targets },
556        headers: { 'X-CSRFToken' : $.cookie('csrftoken')},
557        success: function (_data) {
558          if (_data.error !== "ok") {
559            console.warn(_data.error);
560          } else {
561            if (onsuccess !== undefined) onsuccess(_data);
562          }
563        },
564        error: function (_data) {
565          console.warn("Call failed");
566          console.warn(_data);
567          if (onfail) onfail(data);
568    } });
569  }
570
571  return {
572    enableAjaxLoadingTimer: _enableAjaxLoadingTimer,
573    disableAjaxLoadingTimer: _disableAjaxLoadingTimer,
574    reload_params : reload_params,
575    startABuild : _startABuild,
576    cancelABuild : _cancelABuild,
577    getMostRecentBuilds: _getMostRecentBuilds,
578    makeTypeahead : _makeTypeahead,
579    getProjectInfo: _getProjectInfo,
580    getLayerDepsForProject : _getLayerDepsForProject,
581    editCurrentProject : _editCurrentProject,
582    debug: false,
583    parseUrlParams : _parseUrlParams,
584    dumpsUrlParams : _dumpsUrlParams,
585    addRmLayer : _addRmLayer,
586    makeLayerAddRmAlertMsg : _makeLayerAddRmAlertMsg,
587    showChangeNotification : _showChangeNotification,
588    createCustomRecipe: _createCustomRecipe,
589    makeProjectNameValidation: _makeProjectNameValidation,
590    setNotification: _setNotification,
591    updateProject : _updateProject,
592    cancelProject : _cancelProject,
593    setDefaultImage : _setDefaultImage,
594  };
595})();
596
597/* keep this in the global scope for compatability */
598function reload_params(params) {
599    var uri = window.location.href;
600    var splitlist = uri.split("?");
601    var url = splitlist[0];
602    var parameters = splitlist[1];
603    // deserialize the call parameters
604    var cparams = [];
605    if(parameters)
606      cparams = parameters.split("&");
607
608    var nparams = {};
609    for (var i = 0; i < cparams.length; i++) {
610        var temp = cparams[i].split("=");
611        nparams[temp[0]] = temp[1];
612    }
613    // update parameter values
614    for (i in params) {
615        nparams[encodeURIComponent(i)] = encodeURIComponent(params[i]);
616    }
617    // serialize the structure
618    var callparams = [];
619    for (i in nparams) {
620        callparams.push(i+"="+nparams[i]);
621    }
622    window.location.href = url+"?"+callparams.join('&');
623}
624
625/* Things that happen for all pages */
626$(document).ready(function() {
627
628  (function showNotificationRequest(){
629    var cookie = $.cookie('toaster-notification');
630
631    if (!cookie)
632      return;
633
634    var notificationData = JSON.parse(cookie);
635
636    libtoaster.showChangeNotification(notificationData.message);
637
638    $.removeCookie('toaster-notification', { path: "/"});
639  })();
640
641
642
643  var ajaxLoadingTimer;
644
645  /* If we don't have a console object which might be the case in some
646     * browsers, no-op it to avoid undefined errors.
647     */
648    if (!window.console) {
649      window.console = {};
650      window.console.warn = function() {};
651      window.console.error = function() {};
652    }
653
654    /*
655     * highlight plugin.
656     */
657    hljs.initHighlightingOnLoad();
658
659    // Prevent invalid links from jumping page scroll
660    $('a[href="#"]').click(function() {
661        return false;
662    });
663
664
665    /* START TODO Delete this section now redundant */
666    /* Belen's additions */
667
668    // turn Edit columns dropdown into a multiselect menu
669    $('.dropdown-menu input, .dropdown-menu label').click(function(e) {
670        e.stopPropagation();
671    });
672
673    // enable popovers in any table cells that contain an anchor with the
674    // .btn class applied, and make sure popovers work on click, are mutually
675    // exclusive and they close when your click outside their area
676
677    $('html').click(function(){
678        $('td > a.btn').popover('hide');
679    });
680
681    $('td > a.btn').popover({
682        html:true,
683        placement:'left',
684        container:'body',
685        trigger:'manual'
686    }).click(function(e){
687        $('td > a.btn').not(this).popover('hide');
688        // ideally we would use 'toggle' here
689        // but it seems buggy in our Bootstrap version
690        $(this).popover('show');
691        e.stopPropagation();
692    });
693
694    // enable tooltips for applied filters
695    $('th a.btn-primary').tooltip({container:'body', html:true, placement:'bottom', delay:{hide:1500}});
696
697    // hide applied filter tooltip when you click on the filter button
698    $('th a.btn-primary').click(function () {
699        $('.tooltip').hide();
700    });
701
702    /* Initialise bootstrap tooltips */
703    $(".get-help, [data-toggle=tooltip]").tooltip({
704      container : 'body',
705      html : true,
706      delay: { show : 300 }
707    });
708
709    // show help bubble on hover inside tables
710    $("table").on("mouseover", "th, td", function () {
711        $(this).find(".hover-help").css("visibility","visible");
712    });
713
714    $("table").on("mouseleave", "th, td", function () {
715        $(this).find(".hover-help").css("visibility","hidden");
716    });
717
718    /* END TODO Delete this section now redundant */
719
720    // show task type and outcome in task details pages
721    $(".task-info").tooltip({ container: 'body', html: true, delay: {show: 200}, placement: 'right' });
722
723    // initialise the tooltips for the edit icons
724    $(".glyphicon-edit").tooltip({ container: 'body', html: true, delay: {show: 400}, title: "Change" });
725
726    // initialise the tooltips for the download icons
727    $(".icon-download-alt").tooltip({ container: 'body', html: true, delay: { show: 200 } });
728
729    // initialise popover for debug information
730    $(".glyphicon-info-sign").popover( { placement: 'bottom', html: true, container: 'body' });
731
732    // linking directly to tabs
733    $(function(){
734          var hash = window.location.hash;
735          $('ul.nav a[href="' + hash + '"]').tab('show');
736
737          $('.nav-tabs a').click(function () {
738            $(this).tab('show');
739            $('body').scrollTop();
740          });
741    });
742
743    // toggle for long content (variables, python stack trace, etc)
744    $('.full, .full-hide').hide();
745    $('.full-show').click(function(){
746        $('.full').slideDown(function(){
747            $('.full-hide').show();
748        });
749        $(this).hide();
750    });
751    $('.full-hide').click(function(){
752        $(this).hide();
753        $('.full').slideUp(function(){
754            $('.full-show').show();
755        });
756    });
757
758    //toggle the errors and warnings sections
759    $('.show-errors').click(function() {
760        $('#collapse-errors').addClass('in');
761    });
762    $('.toggle-errors').click(function() {
763        $('#collapse-errors').toggleClass('in');
764    });
765    $('.show-warnings').click(function() {
766        $('#collapse-warnings').addClass('in');
767    });
768    $('.toggle-warnings').click(function() {
769        $('#collapse-warnings').toggleClass('in');
770    });
771    $('.show-exceptions').click(function() {
772        $('#collapse-exceptions').addClass('in');
773    });
774    $('.toggle-exceptions').click(function() {
775        $('#collapse-exceptions').toggleClass('in');
776    });
777
778
779    $("#hide-alert").click(function(){
780      $(this).parent().fadeOut();
781    });
782
783    //show warnings section when requested from the previous page
784    if (location.href.search('#warnings') > -1) {
785        $('#collapse-warnings').addClass('in');
786    }
787
788    /* Show the loading notification if nothing has happend after 1.5
789     * seconds
790     */
791    $(document).bind("ajaxStart", function(){
792      if (ajaxLoadingTimer)
793        window.clearTimeout(ajaxLoadingTimer);
794
795      ajaxLoadingTimer = window.setTimeout(function() {
796        if (libtoaster.ajaxLoadingTimerEnabled) {
797          $("#loading-notification").fadeIn();
798        }
799      }, 1200);
800    });
801
802    $(document).bind("ajaxStop", function(){
803      if (ajaxLoadingTimer)
804        window.clearTimeout(ajaxLoadingTimer);
805
806      $("#loading-notification").fadeOut();
807    });
808
809    $(document).ajaxError(function(event, jqxhr, settings, errMsg){
810      if (errMsg === 'abort')
811        return;
812
813      console.warn("Problem with xhr call");
814      console.warn(errMsg);
815      console.warn(jqxhr.responseText);
816    });
817
818    function check_for_duplicate_ids () {
819      /* warn about duplicate element ids */
820      var ids = {};
821      $("[id]").each(function() {
822        if (this.id && ids[this.id]) {
823          console.warn('Duplicate element id #'+this.id);
824        }
825        ids[this.id] = true;
826      });
827    }
828
829    /* Make sure we don't have a notification overlay a modal */
830    $(".modal").on('show.bs.modal', function(){
831      $(".alert-dismissible").fadeOut();
832    });
833
834    if (libtoaster.debug) {
835      check_for_duplicate_ids();
836    } else {
837      /* Debug is false so supress warnings by overriding the functions */
838      window.console.warn = function () {};
839      window.console.error = function () {};
840   }
841});
842