<commit_before>/* $Id$ */

/**
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements. See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

'use strict';

//Make sure jQuery has been loaded before app.js
if (typeof jQuery === "undefined")
{
  throw new Error("ManifoldCF requires jQuery");
}

/* ManifoldCF
 *
 * @type Object
 * @description $.ManifoldCF is the main object for the template's app.
 *              It's used for implementing functions and options related
 *              to the template. Keeping everything wrapped in an object
 *              prevents conflict with other plugins and is a better
 *              way to organize our code.
 */
$.ManifoldCF={};

/* --------------------
 * - ManifoldCF Options -
 * --------------------
 * Modify these options to suit your implementation
 */
$.ManifoldCF.options={
  //Sidebar push menu toggle button selector
  sidebarToggleSelector: "[data-toggle='offcanvas']",
  //Activate sidebar slimscroll if the fixed layout is set (requires SlimScroll Plugin)
  sidebarSlimScroll: true,
  BSTooltipSelector: '[data-toggle="tooltip"]',
  //The standard screen sizes that bootstrap uses.
  //If you change these in the variables.less file, change
  //them here too.
  screenSizes: {
    xs: 480,
    sm: 768,
    md: 992,
    lg: 1200
  }
};

/* ------------------
 * - Implementation -
 * ------------------
 * The next block of code implements ManifoldCF's
 * functions and plugins as specified by the
 * options above.
 */
$(function ()
{
  //Easy access to options
  var o=$.ManifoldCF.options;

  //Set up the object
  _init();

  //Activate the layout maker
  $.ManifoldCF.layout.activate();

  //Enable sidebar tree view controls
  $.ManifoldCF.tree('.sidebar');

  //Activate sidebar push menu
  $.ManifoldCF.pushMenu.activate(o.sidebarToggleSelector);

  //Activate Bootstrap tooltip
  $(o.BSTooltipSelector).tooltip({
    trigger: 'hover',
    container: 'body'
  });

  /*
   * INITIALIZE BUTTON TOGGLE
   * ------------------------
   */
  $('.btn-group[data-toggle="btn-toggle"]').each(function ()
  {
    var group=$(this);
    $(this).find(".btn").click(function (e)
    {
      group.find(".btn.active").removeClass("active");
      $(this).addClass("active");
      e.preventDefault();
    });

  });
});

/* ----------------------------------
 * - Initialize the ManifoldCF Object -
 * ----------------------------------
 * All ManifoldCF functions are implemented below.
 */
function _init()
{

  /* Layout
   * ======
   * Fixes the layout height in case min-height fails.
   *
   * @type Object
   * @usage $.ManifoldCF.layout.activate()
   *        $.ManifoldCF.layout.fix()
   *        $.ManifoldCF.layout.fixSidebar()
   */
  $.ManifoldCF.layout={
    activate: function ()
    {
      var _this=this;
      _this.fix();
      _this.fixSidebar();
      $(window,".wrapper").resize(function ()
      {
        _this.fix();
        _this.fixSidebar();
      });
    },
    fix: function ()
    {
      //Get window height and the wrapper height
      var neg=$('.main-header').outerHeight() + $('.main-footer').outerHeight();
      var window_height=$(window).height();
      var sidebar_height=$(".sidebar").height();
      //Set the min-height of the content and sidebar based on the
      //the height of the document.
      if ($("body").hasClass("fixed"))
      {
        $(".content-wrapper, .right-side").css('min-height',window_height - $('.main-footer').outerHeight());
      } else
      {
        var postSetWidth;
        if (window_height >= sidebar_height)
        {
          $(".content-wrapper, .right-side").css('min-height',window_height - neg);
          postSetWidth=window_height - neg;
        } else
        {
          $(".content-wrapper, .right-side").css('min-height',sidebar_height);
          postSetWidth=sidebar_height;
        }
      }
      $('.main-footer').show();
    },
    fixSidebar: function ()
    {
      //Make sure the body tag has the .fixed class
      if (!$("body").hasClass("fixed"))
      {
        if (typeof $.fn.slimScroll != 'undefined')
        {
          $(".sidebar").slimScroll({destroy: true}).height("auto");
        }
        return;
      } else if (typeof $.fn.slimScroll == 'undefined' && console)
      {
        console.error("Error: the fixed layout requires the slimscroll plugin!");
      }
      //Enable slimscroll for fixed layout
      if ($.ManifoldCF.options.sidebarSlimScroll)
      {
        if (typeof $.fn.slimScroll != 'undefined')
        {
          //Destroy if it exists
          $(".sidebar").slimScroll({destroy: true}).height("auto");
          //Add slimscroll
          $(".sidebar").slimscroll({
            height: ($(window).height() - $(".main-header").height()) + "px",
            color: "rgba(255,255,255,0.8)",
            size: "5px"
          });
        }
      }
    }
  };

  /* PushMenu()
   * ==========
   * Adds the push menu functionality to the sidebar.
   *
   * @type Function
   * @usage: $.ManifoldCF.pushMenu("[data-toggle='offcanvas']")
   */
  $.ManifoldCF.pushMenu={
    activate: function (toggleBtn)
    {
      //Get the screen sizes
      var screenSizes=$.ManifoldCF.options.screenSizes;

      //Enable sidebar toggle
      $(toggleBtn).on('click',function (e)
      {
        e.preventDefault();

        //Enable sidebar push menu
        if ($(window).width() > (screenSizes.sm - 1))
        {
          $("body").toggleClass('sidebar-collapse');
        }
        //Handle sidebar push menu for small screens
        else
        {
          if ($("body").hasClass('sidebar-open'))
          {
            $("body").removeClass('sidebar-open');
            $("body").removeClass('sidebar-collapse')
          } else
          {
            $("body").addClass('sidebar-open');
          }
        }
      });

      $(".content-wrapper").click(function ()
      {
        //Enable hide menu when clicking on the content-wrapper on small screens
        if ($(window).width() <= (screenSizes.sm - 1) && $("body").hasClass("sidebar-open"))
        {
          $("body").removeClass('sidebar-open');
        }
      });

      //Enable expand on hover for sidebar mini
      if ($.ManifoldCF.options.sidebarExpandOnHover
          || ($('body').hasClass('fixed')
          && $('body').hasClass('sidebar-mini')))
      {
        this.expandOnHover();
      }

    },
    expandOnHover: function ()
    {
      var _this=this;
      var screenWidth=$.ManifoldCF.options.screenSizes.sm - 1;
      //Expand sidebar on hover
      $('.main-sidebar').hover(function ()
      {
        if ($('body').hasClass('sidebar-mini')
            && $("body").hasClass('sidebar-collapse')
            && $(window).width() > screenWidth)
        {
          _this.expand();
        }
      },function ()
      {
        if ($('body').hasClass('sidebar-mini')
            && $('body').hasClass('sidebar-expanded-on-hover')
            && $(window).width() > screenWidth)
        {
          _this.collapse();
        }
      });
    },
    expand: function ()
    {
      $("body").removeClass('sidebar-collapse').addClass('sidebar-expanded-on-hover');
    },
    collapse: function ()
    {
      if ($('body').hasClass('sidebar-expanded-on-hover'))
      {
        $('body').removeClass('sidebar-expanded-on-hover').addClass('sidebar-collapse');
      }
    }
  };

  /* Tree()
   * ======
   * Converts the sidebar into a multilevel
   * tree view menu.
   *
   * @type Function
   * @Usage: $.ManifoldCF.tree('.sidebar')
   */
  $.ManifoldCF.tree=function (menu)
  {
    var _this=this;

    $("li a",$(menu)).on('click',function (e)
    {
      //Get the clicked link and the next element
      var $this=$(this);
      var checkElement=$this.next();

      //Check if the next element is a menu and is visible
      if ((checkElement.is('.treeview-menu')) && (checkElement.is(':visible')))
      {
        //Close the menu
        checkElement.slideUp('normal',function ()
        {
          checkElement.removeClass('menu-open');
          //Fix the layout in case the sidebar stretches over the height of the window
          //_this.layout.fix();
        });
        checkElement.parent("li").removeClass("active");
      }
      //If the menu is not visible
      else if ((checkElement.is('.treeview-menu')) && (!checkElement.is(':visible')))
      {
        //Get the parent menu
        var parent=$this.parents('ul').first();
        //Close all open menus within the parent
        var ul=parent.find('ul:visible').slideUp('normal');
        //Remove the menu-open class from the parent
        ul.removeClass('menu-open');
        //Get the parent li
        var parent_li=$this.parent("li");

        //Open the target menu and add the menu-open class
        checkElement.slideDown('normal',function ()
        {
          //Add the class active to the parent li
          checkElement.addClass('menu-open');
          parent.find('li.active').removeClass('active');
          parent_li.addClass('active');
          //Fix the layout in case the sidebar stretches over the height of the window
          _this.layout.fix();
        });
      }
      //if this isn't a link, prevent the page from being redirected
      if (checkElement.is('.treeview-menu'))
      {
        e.preventDefault();
      }
    });
  };
}

$.ManifoldCF.setTitle=function (title,header,activeMenu)
{
  document.title=title;

  $(".content-header #heading").text(header);

  activeMenu=typeof activeMenu !== 'undefined'?activeMenu:'outputs';
  $("." + activeMenu).addClass("active");

  $(".selectpicker").selectpicker({iconBase:'fa',tickIcon:'fa-check'});
};

function displayError(xhr)
{
  $(".content-header #heading").text('Error!');
  document.title='Error';
  var msg = xhr.status + " " + xhr.statusText;
  //Proper error message, if the server is down.
  if(xhr.status === 0)
  {
    msg = MCFError.ServerDown;
  }
  else if(xhr.status === 500)
  {
    msg = MCFError.InternalServerError;
  }
  var errorTemplate = '
' +
                        '
' +
                          '
' +
                            '
 Error!
' +
                            '
' + msg +'
' +
                          '
' +
                        '
' +
                        '
' +
                          '
' +
                            '' +
                              'OK' +
                            '';
  if(xhr.status === 500)
  {
    errorTemplate +=        '<button type="button" class="btn btn-default" data-toggle="modal" data-target="#exceptionModal">' +
                              'See Exception</button>';
    $('#exceptionModal .modal-body').empty();
    $('#exceptionModal .modal-body').html(xhr.responseText);
  }
  errorTemplate +=        '
' +
                        '
' +
                      '
';
  $("#content").html(errorTemplate);
}

function _endsWith(str, suffix) {
    return str.indexOf(suffix, str.length - suffix.length) !== -1;
}

function _preLoadContent()
{
  console.log('_preLoadContent');
  $($.ManifoldCF.options.BSTooltipSelector).tooltip('destroy');

  //Temporary fix for Javascript bug, when loading connector javascript.
  //Get all the global methods
  var methods = Object.keys(window);
  for(var i=0; i<=methods.length; i++){
    var method = methods[i];
      if (method && (_endsWith(method, 'checkConfig') ||
          _endsWith(method, 'checkConfigForSave') ||
          _endsWith(method, 'checkSpecification') ||
          _endsWith(method, 'checkSpecificationForSave'))) {
          delete window[method];
      }
  }
}

function _postLoadContent()
{
  console.log('_postLoadContent');
  //Activate Bootstrap tooltip
  $($.ManifoldCF.options.BSTooltipSelector).tooltip({
    trigger: 'hover',
    container: 'body'
  });
}

$.ManifoldCF.loadContent=function (url)
{
  $('.overlay,#loader').show();
  console.log("URL: " + url);
  _preLoadContent();
  $('#content').load(decodeURIComponent(url),function (response,status,xhr)
  {
    if (status == 'error')
    {
      displayError(xhr);
    }
    _postLoadContent();
    $('.overlay,#loader').hide();
  });
};

$.ManifoldCF.submit=function (form)
{
  $('.overlay,#loader').show();

  var formData = new FormData(form),
      $form = $(form),
      action = $form.attr('action'),
      method = $form.attr('method');

  _preLoadContent();

  //Add Client Timezone Offset
  var timezone_offset = new Date().getTimezoneOffset();
  var timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
  formData.set('client_timezone_offset',timezone_offset);
  formData.set('client_timezone',timezone);

  $.ajax({
    type: method,
    url: action,
    data: formData,
    cache: false,
    contentType: false,
    processData: false
  }).done(function (data,textStatus,jqXHR)
  {
    var page=jqXHR.getResponseHeader("page");
    console.log("page: " + page)
    if (typeof page != 'undefined')
    {
      window.history.replaceState({urlPath: encodeURI(page)},null,'?p=' + page + '#execute');
    }
    else
    {
      window.history.replaceState({urlPath: encodeURI(action)},null,'#execute_' + form.name);
    }
    console.log("textStatus: " + textStatus);
    $('#content').html(data);
  }).fail(function (xhr,opts,error)
  {
    displayError(xhr);
  }).always(function ()
  {
    _postLoadContent();
    $('.overlay,#loader').hide();
  });
}

window.onpopstate = function (event) {
  console.log('historyEvent:',event);
  if (event.state) {
    // history changed because of pushState/replaceState
    var state=window.history.state;
    if (typeof  state != 'undefined')
    {
      if (!state.urlPath.startsWith('execute'))
      {
        $.ManifoldCF.loadContent(state.urlPath);
      }
    }
  }
  else
  {
    console.log('history changed because of a page load');
  }
}

$(function(){
  // navigation link handler
  $(document.body).on("click",'.link',function (e)
  {
    e.preventDefault();
    var urlPath=$(this).attr('href');
    var title=$(this).text();

    $.ManifoldCF.loadContent(urlPath);

    var data = {urlPath: encodeURIComponent(urlPath)};
    window.history.pushState(data,title,'?p=' + encodeURIComponent(urlPath) + '&_' + new Date().getTime());
  });
});
<commit_msg>Final Fix for adding Client Timezone<commit_after>/* $Id$ */

/**
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements. See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

'use strict';

//Make sure jQuery has been loaded before app.js
if (typeof jQuery === "undefined")
{
  throw new Error("ManifoldCF requires jQuery");
}

/* ManifoldCF
 *
 * @type Object
 * @description $.ManifoldCF is the main object for the template's app.
 *              It's used for implementing functions and options related
 *              to the template. Keeping everything wrapped in an object
 *              prevents conflict with other plugins and is a better
 *              way to organize our code.
 */
$.ManifoldCF={};

/* --------------------
 * - ManifoldCF Options -
 * --------------------
 * Modify these options to suit your implementation
 */
$.ManifoldCF.options={
  //Sidebar push menu toggle button selector
  sidebarToggleSelector: "[data-toggle='offcanvas']",
  //Activate sidebar slimscroll if the fixed layout is set (requires SlimScroll Plugin)
  sidebarSlimScroll: true,
  BSTooltipSelector: '[data-toggle="tooltip"]',
  //The standard screen sizes that bootstrap uses.
  //If you change these in the variables.less file, change
  //them here too.
  screenSizes: {
    xs: 480,
    sm: 768,
    md: 992,
    lg: 1200
  }
};

/* ------------------
 * - Implementation -
 * ------------------
 * The next block of code implements ManifoldCF's
 * functions and plugins as specified by the
 * options above.
 */
$(function ()
{
  //Easy access to options
  var o=$.ManifoldCF.options;

  //Set up the object
  _init();

  //Activate the layout maker
  $.ManifoldCF.layout.activate();

  //Enable sidebar tree view controls
  $.ManifoldCF.tree('.sidebar');

  //Activate sidebar push menu
  $.ManifoldCF.pushMenu.activate(o.sidebarToggleSelector);

  //Activate Bootstrap tooltip
  $(o.BSTooltipSelector).tooltip({
    trigger: 'hover',
    container: 'body'
  });

  /*
   * INITIALIZE BUTTON TOGGLE
   * ------------------------
   */
  $('.btn-group[data-toggle="btn-toggle"]').each(function ()
  {
    var group=$(this);
    $(this).find(".btn").click(function (e)
    {
      group.find(".btn.active").removeClass("active");
      $(this).addClass("active");
      e.preventDefault();
    });

  });
});

/* ----------------------------------
 * - Initialize the ManifoldCF Object -
 * ----------------------------------
 * All ManifoldCF functions are implemented below.
 */
function _init()
{

  /* Layout
   * ======
   * Fixes the layout height in case min-height fails.
   *
   * @type Object
   * @usage $.ManifoldCF.layout.activate()
   *        $.ManifoldCF.layout.fix()
   *        $.ManifoldCF.layout.fixSidebar()
   */
  $.ManifoldCF.layout={
    activate: function ()
    {
      var _this=this;
      _this.fix();
      _this.fixSidebar();
      $(window,".wrapper").resize(function ()
      {
        _this.fix();
        _this.fixSidebar();
      });
    },
    fix: function ()
    {
      //Get window height and the wrapper height
      var neg=$('.main-header').outerHeight() + $('.main-footer').outerHeight();
      var window_height=$(window).height();
      var sidebar_height=$(".sidebar").height();
      //Set the min-height of the content and sidebar based on the
      //the height of the document.
      if ($("body").hasClass("fixed"))
      {
        $(".content-wrapper, .right-side").css('min-height',window_height - $('.main-footer').outerHeight());
      } else
      {
        var postSetWidth;
        if (window_height >= sidebar_height)
        {
          $(".content-wrapper, .right-side").css('min-height',window_height - neg);
          postSetWidth=window_height - neg;
        } else
        {
          $(".content-wrapper, .right-side").css('min-height',sidebar_height);
          postSetWidth=sidebar_height;
        }
      }
      $('.main-footer').show();
    },
    fixSidebar: function ()
    {
      //Make sure the body tag has the .fixed class
      if (!$("body").hasClass("fixed"))
      {
        if (typeof $.fn.slimScroll != 'undefined')
        {
          $(".sidebar").slimScroll({destroy: true}).height("auto");
        }
        return;
      } else if (typeof $.fn.slimScroll == 'undefined' && console)
      {
        console.error("Error: the fixed layout requires the slimscroll plugin!");
      }
      //Enable slimscroll for fixed layout
      if ($.ManifoldCF.options.sidebarSlimScroll)
      {
        if (typeof $.fn.slimScroll != 'undefined')
        {
          //Destroy if it exists
          $(".sidebar").slimScroll({destroy: true}).height("auto");
          //Add slimscroll
          $(".sidebar").slimscroll({
            height: ($(window).height() - $(".main-header").height()) + "px",
            color: "rgba(255,255,255,0.8)",
            size: "5px"
          });
        }
      }
    }
  };

  /* PushMenu()
   * ==========
   * Adds the push menu functionality to the sidebar.
   *
   * @type Function
   * @usage: $.ManifoldCF.pushMenu("[data-toggle='offcanvas']")
   */
  $.ManifoldCF.pushMenu={
    activate: function (toggleBtn)
    {
      //Get the screen sizes
      var screenSizes=$.ManifoldCF.options.screenSizes;

      //Enable sidebar toggle
      $(toggleBtn).on('click',function (e)
      {
        e.preventDefault();

        //Enable sidebar push menu
        if ($(window).width() > (screenSizes.sm - 1))
        {
          $("body").toggleClass('sidebar-collapse');
        }
        //Handle sidebar push menu for small screens
        else
        {
          if ($("body").hasClass('sidebar-open'))
          {
            $("body").removeClass('sidebar-open');
            $("body").removeClass('sidebar-collapse')
          } else
          {
            $("body").addClass('sidebar-open');
          }
        }
      });

      $(".content-wrapper").click(function ()
      {
        //Enable hide menu when clicking on the content-wrapper on small screens
        if ($(window).width() <= (screenSizes.sm - 1) && $("body").hasClass("sidebar-open"))
        {
          $("body").removeClass('sidebar-open');
        }
      });

      //Enable expand on hover for sidebar mini
      if ($.ManifoldCF.options.sidebarExpandOnHover
          || ($('body').hasClass('fixed')
          && $('body').hasClass('sidebar-mini')))
      {
        this.expandOnHover();
      }

    },
    expandOnHover: function ()
    {
      var _this=this;
      var screenWidth=$.ManifoldCF.options.screenSizes.sm - 1;
      //Expand sidebar on hover
      $('.main-sidebar').hover(function ()
      {
        if ($('body').hasClass('sidebar-mini')
            && $("body").hasClass('sidebar-collapse')
            && $(window).width() > screenWidth)
        {
          _this.expand();
        }
      },function ()
      {
        if ($('body').hasClass('sidebar-mini')
            && $('body').hasClass('sidebar-expanded-on-hover')
            && $(window).width() > screenWidth)
        {
          _this.collapse();
        }
      });
    },
    expand: function ()
    {
      $("body").removeClass('sidebar-collapse').addClass('sidebar-expanded-on-hover');
    },
    collapse: function ()
    {
      if ($('body').hasClass('sidebar-expanded-on-hover'))
      {
        $('body').removeClass('sidebar-expanded-on-hover').addClass('sidebar-collapse');
      }
    }
  };

  /* Tree()
   * ======
   * Converts the sidebar into a multilevel
   * tree view menu.
   *
   * @type Function
   * @Usage: $.ManifoldCF.tree('.sidebar')
   */
  $.ManifoldCF.tree=function (menu)
  {
    var _this=this;

    $("li a",$(menu)).on('click',function (e)
    {
      //Get the clicked link and the next element
      var $this=$(this);
      var checkElement=$this.next();

      //Check if the next element is a menu and is visible
      if ((checkElement.is('.treeview-menu')) && (checkElement.is(':visible')))
      {
        //Close the menu
        checkElement.slideUp('normal',function ()
        {
          checkElement.removeClass('menu-open');
          //Fix the layout in case the sidebar stretches over the height of the window
          //_this.layout.fix();
        });
        checkElement.parent("li").removeClass("active");
      }
      //If the menu is not visible
      else if ((checkElement.is('.treeview-menu')) && (!checkElement.is(':visible')))
      {
        //Get the parent menu
        var parent=$this.parents('ul').first();
        //Close all open menus within the parent
        var ul=parent.find('ul:visible').slideUp('normal');
        //Remove the menu-open class from the parent
        ul.removeClass('menu-open');
        //Get the parent li
        var parent_li=$this.parent("li");

        //Open the target menu and add the menu-open class
        checkElement.slideDown('normal',function ()
        {
          //Add the class active to the parent li
          checkElement.addClass('menu-open');
          parent.find('li.active').removeClass('active');
          parent_li.addClass('active');
          //Fix the layout in case the sidebar stretches over the height of the window
          _this.layout.fix();
        });
      }
      //if this isn't a link, prevent the page from being redirected
      if (checkElement.is('.treeview-menu'))
      {
        e.preventDefault();
      }
    });
  };
}

$.ManifoldCF.setTitle=function (title,header,activeMenu)
{
  document.title=title;

  $(".content-header #heading").text(header);

  activeMenu=typeof activeMenu !== 'undefined'?activeMenu:'outputs';
  $("." + activeMenu).addClass("active");

  $(".selectpicker").selectpicker({iconBase:'fa',tickIcon:'fa-check'});
};

function displayError(xhr)
{
  $(".content-header #heading").text('Error!');
  document.title='Error';
  var msg = xhr.status + " " + xhr.statusText;
  //Proper error message, if the server is down.
  if(xhr.status === 0)
  {
    msg = MCFError.ServerDown;
  }
  else if(xhr.status === 500)
  {
    msg = MCFError.InternalServerError;
  }
  var errorTemplate = '
' +
                        '
' +
                          '
' +
                            '
 Error!
' +
                            '
' + msg +'
' +
                          '
' +
                        '
' +
                        '
' +
                          '
' +
                            '' +
                              'OK' +
                            '';
  if(xhr.status === 500)
  {
    errorTemplate +=        '<button type="button" class="btn btn-default" data-toggle="modal" data-target="#exceptionModal">' +
                              'See Exception</button>';
    $('#exceptionModal .modal-body').empty();
    $('#exceptionModal .modal-body').html(xhr.responseText);
  }
  errorTemplate +=        '
' +
                        '
' +
                      '
';
  $("#content").html(errorTemplate);
}

function _endsWith(str, suffix) {
    return str.indexOf(suffix, str.length - suffix.length) !== -1;
}

function _preLoadContent()
{
  console.log('_preLoadContent');
  $($.ManifoldCF.options.BSTooltipSelector).tooltip('destroy');

  //Temporary fix for Javascript bug, when loading connector javascript.
  //Get all the global methods
  var methods = Object.keys(window);
  for(var i=0; i<=methods.length; i++){
    var method = methods[i];
      if (method && (_endsWith(method, 'checkConfig') ||
          _endsWith(method, 'checkConfigForSave') ||
          _endsWith(method, 'checkSpecification') ||
          _endsWith(method, 'checkSpecificationForSave'))) {
          delete window[method];
      }
  }
}

function _postLoadContent()
{
  console.log('_postLoadContent');
  //Activate Bootstrap tooltip
  $($.ManifoldCF.options.BSTooltipSelector).tooltip({
    trigger: 'hover',
    container: 'body'
  });
}

$.ManifoldCF.loadContent=function (url)
{
  $('.overlay,#loader').show();
  console.log("URL: " + url);
  _preLoadContent();

  //Add Client Timezone Offset
  var timezone_offset = new Date().getTimezoneOffset();
  var timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;

  $('#content').load(decodeURIComponent(url), { client_timezone_offset : timezone_offset, client_timezone : timezone } ,function (response,status,xhr)
  {
    if (status == 'error')
    {
      displayError(xhr);
    }
    _postLoadContent();
    $('.overlay,#loader').hide();
  });
};

$.ManifoldCF.submit=function (form)
{
  $('.overlay,#loader').show();

  var formData = new FormData(form),
      $form = $(form),
      action = $form.attr('action'),
      method = $form.attr('method');

  _preLoadContent();

  //Add Client Timezone Offset
  var timezone_offset = new Date().getTimezoneOffset();
  var timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
  formData.set('client_timezone_offset',timezone_offset);
  formData.set('client_timezone',timezone);

  $.ajax({
    type: method,
    url: action,
    data: formData,
    cache: false,
    contentType: false,
    processData: false
  }).done(function (data,textStatus,jqXHR)
  {
    var page=jqXHR.getResponseHeader("page");
    console.log("page: " + page)
    if (typeof page != 'undefined')
    {
      window.history.replaceState({urlPath: encodeURI(page)},null,'?p=' + page + '#execute');
    }
    else
    {
      window.history.replaceState({urlPath: encodeURI(action)},null,'#execute_' + form.name);
    }
    console.log("textStatus: " + textStatus);
    $('#content').html(data);
  }).fail(function (xhr,opts,error)
  {
    displayError(xhr);
  }).always(function ()
  {
    _postLoadContent();
    $('.overlay,#loader').hide();
  });
}

window.onpopstate = function (event) {
  console.log('historyEvent:',event);
  if (event.state) {
    // history changed because of pushState/replaceState
    var state=window.history.state;
    if (typeof  state != 'undefined')
    {
      if (!state.urlPath.startsWith('execute'))
      {
        $.ManifoldCF.loadContent(state.urlPath);
      }
    }
  }
  else
  {
    console.log('history changed because of a page load');
  }
}

$(function(){
  // navigation link handler
  $(document.body).on("click",'.link',function (e)
  {
    e.preventDefault();
    var urlPath=$(this).attr('href');
    var title=$(this).text();

    $.ManifoldCF.loadContent(urlPath);

    var data = {urlPath: encodeURIComponent(urlPath)};
    window.history.pushState(data,title,'?p=' + encodeURIComponent(urlPath) + '&_' + new Date().getTime());
  });
});
<|endoftext|>


<commit_before>/*!
 * Taiwan Village Picker
 * Copyright 2016 Jam Chen
 * MIT License (https://raw.githubusercontent.com/Dogfalo/materialize/master/LICENSE)
 */
// Check for jQuery.
if (typeof(jQuery) === 'undefined') {
  var jQuery;
  // Check if require is a defined function.
  if (typeof(require) === 'function') {
    jQuery = $ = require('jquery');
  // Else use the dollar sign alias.
  } else if ($) {
    jQuery = $;
  }
}

;(function ($) {

	var village_data;
	var parseResponse = function parseResponse(res) {
		// 直轄市採兩碼，直轄市所屬之區採五碼，村里採三碼；
		// 縣轄市採五碼，縣轄市所屬之市、鎮、鄉及區採兩碼，村里採三碼，
		// 名稱及代碼順序依內政部提供者為準
		//
		// 歸納:
		// - level 1: 兩碼或五碼
		// - level 2: 共七碼
		// - level 3: 共十碼（包含'-'共十一個字元）
		var findParentForCode = function findParentForCode(village_data, code) {
			if (2 === code.length || 5 === code.length) {
				return village_data;
			}
			var parentNode = village_data[code.substring(0, 2)] || village_data[code.substring(0, 5)];
			if (7 === code.length) {
				return parentNode?parentNode.children:parentNode;
			} else if (11 === code.length) {
				parentNode = parentNode.children[code.substring(0, 7)];
				return parentNode?parentNode.children:parentNode;
			} else {
				console.log('Illeagle code length:', code);
			}
		};
		var isLeaf = function(code) {
			return 11 === code.length;
		};
		var village_data = {};
		var lastIndex;
		$(res).find('Row').each(function(index, el) {
			var $el = $(el);
			var code = $el.find('Code').text().trim();
			var content = $el.find('Content').text().trim();
			var parentNode = findParentForCode(village_data, code);
			if (parentNode) {
				parentNode[code] = {
					code: code,
					content: content,
					children: isLeaf(code)?undefined:{}
				};
			} else {
				console.log('error: find no parent node for ', code, content);
			}
			lastIndex = index;
		});
		console.log('total row:', lastIndex+1);
		return village_data;
	}
	var loadVillageData = function loadVillageData(options) {
		var dfd = $.Deferred();
		if (village_data) {
			dfd.resolve(village_data);
		} else {
			$.ajax({
				url: '/raw/village.xml',
				dataType: 'xml'
			})
			.done(function(res) {
				console.log("success", res);
				console.log("parsing xml");
				village_data = parseResponse(res);
				dfd.resolve(village_data);
			})
			.fail(function() {
				dfd.reject.apply(dfd, arguments);
			});

		}
		return dfd.promise();
	};

  $.fn.taiwan_village_picker = function (options) {
  	this.each(function() {
  		var $this = $(this);
  		$.when(loadVillageData(options))
  		.done(function(village_data) {
	  		console.log('taiwan_village_pickerlized', village_data, $this);
  		})
  		.fail(function() {
  			console.log("error occurred", arguments);
  		});

  	});
  };

})(jQuery);<commit_msg>change data structure for Safari performance hit<commit_after>/*!
 * Taiwan Village Picker
 * Copyright 2016 Jam Chen
 * MIT License (https://raw.githubusercontent.com/Dogfalo/materialize/master/LICENSE)
 */
// Check for jQuery.
if (typeof(jQuery) === 'undefined') {
  var jQuery;
  // Check if require is a defined function.
  if (typeof(require) === 'function') {
    jQuery = $ = require('jquery');
  // Else use the dollar sign alias.
  } else if ($) {
    jQuery = $;
  }
}

;(function ($) {

	var village_data;
	var parseResponse = function parseResponse(res) {
		// 直轄市採兩碼，直轄市所屬之區採五碼，村里採三碼；
		// 縣轄市採五碼，縣轄市所屬之市、鎮、鄉及區採兩碼，村里採三碼，
		// 名稱及代碼順序依內政部提供者為準
		//
		// 歸納:
		// - level 1: 兩碼或五碼
		// - level 2: 共七碼
		// - level 3: 共十碼（包含'-'共十一個字元）
		var getParentCode = function getParentCode(parents, code) {
			var parent = $.grep(parents, function(parent) {
					return parent.code === code.substring(0, 2) || parent.code === code.substring(0, 5);
			});
			if (parent.length === 1) {
				return parent[0].code;
			} else {
				console.log('cannot find parent for code', code, ' in ', parents);
			}
		};
		var findParentForCode = function findParentForCode(village_data, code) {
			if (!code) return;
			if (2 === code.length || 5 === code.length) {
				return village_data.level1;
			}
			if (7 === code.length) {
				var parentCode = getParentCode(village_data.level1, code);
				var parent = village_data.level2[parentCode] = village_data.level2[parentCode] || [];
				return parent;
			} else if (11 === code.length) {
				var parentCode = code.substring(0, 7);
				var parent = village_data.level3[parentCode] = village_data.level2[parentCode] || [];
				return parent;
			} else {
				console.log('Illeagle code length:', code);
			}
		};
		var isLeaf = function isLeaf(code) {
			return 11 === code.length;
		};
		var getCodeAndContentFromRow = function getCodeAndContentFromRow(row) {
			var codeAndContent = {};
			for (var i = 0; i < row.childElementCount; i++) {
				var child = row.childNodes[i];
				if ('Code' === child.tagName) {
					codeAndContent.code = child.textContent.trim();
				} else if ('Content' === child.tagName) {
					codeAndContent.content = child.textContent.trim();
				}
			}
			return codeAndContent;
		};
		// 不能用完整樹狀結構，因為不知道為什麼Safari會因此而爆炸慢，只好折衷攤平樹狀結構
		var village_data = {
			level1: [],
			level2: {},
			level3: {}
		};
		$(res).find('Row').each(function(index, el) {
			var $el = $(el);
			var code = $el.find('Code').text().trim();
			var content = $el.find('Content').text().trim();
			var parentNode = findParentForCode(village_data, code);
			if (parentNode) {
				parentNode.push({
					code: code,
					content: content
				});
			} else {
				console.log('error: find no parent node for ', codeAndContent);
			}
		});
		return village_data;
	}
	var loadVillageData = function loadVillageData(options) {
		var dfd = $.Deferred();
		if (village_data) {
			dfd.resolve(village_data);
		} else {
			$.ajax({
				url: 'http://www.dgbas.gov.tw/public/data/open/stat/village.xml', //'/raw/village.xml',
				dataType: 'xml'
			})
			.done(function(res) {
				console.log("parsing xml");
				var start = window.performance?window.performance.now():0;
				window.setTimeout(function() {
					village_data = parseResponse(res);
					if (window.performance) {
						console.log('parsing spent: '+(window.performance.now()-start));
					}
					dfd.resolve(village_data);
				}, 1);
			})
			.fail(function() {
				dfd.reject.apply(dfd, arguments);
			});

		}
		return dfd.promise();
	};

  $.fn.taiwan_village_picker = function (options) {
  	this.each(function() {
  		var $this = $(this);
  		$.when(loadVillageData(options))
  		.done(function(village_data) {
	  		console.log('taiwan_village_pickerlized', village_data, $this);
  		})
  		.fail(function() {
  			console.log("error occurred", arguments);
  		});

  	});
  };

})(jQuery);<|endoftext|>


<commit_before>/*
 * Axelor Business Solutions
 *
 * Copyright (C) 2005-2014 Axelor (<http://axelor.com>).
 *
 * This program is free software: you can redistribute it and/or  modify
 * it under the terms of the GNU Affero General Public License, version 3,
 * as published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
(function() {

var ui = angular.module('axelor.ui');

function TableLayout(items, attrs, $scope, $compile) {

	var colWidths = attrs.widths,
		numCols = +attrs.cols || 4,
		curCol = 0,
		layout = [[]];

	function add(item, label) {

		if (item.is('br')) {
			curCol = 0;
			item.hide();
			return layout.push([]);
		}

		var row = _.last(layout),
			cell = null,
			colspan = +item.attr('x-colspan') || 1,
			rowspan = +item.attr('x-rowspan') || 1;

		if (curCol + colspan >= numCols + 1) {
			curCol = 0, row = [];
			layout.push(row);
		}

		if (label) {
			cell = {};
			cell.elem = label;
			cell.css = label.attr('x-cell-css');
			row.push(cell);
			if (rowspan > 1) cell.rowspan = rowspan;
			if (colspan > 1) colspan -= 1;
			curCol += 1;
		}

		cell = {};
		cell.elem = item;
		cell.css = item.attr('x-cell-css');
		if (colspan > 1) cell.colspan = colspan;
		if (rowspan > 1) cell.rowspan = rowspan;

		row.push(cell);
		curCol += colspan;
	}

	if (colWidths && angular.isString(colWidths)) {
		colWidths = colWidths.trim().split(/\s*,\s*/);
		for(var i = 0 ; i < colWidths.length; i++) {
			var width = colWidths[i];
			if (/^(\d+)$/.test(width)) width = width + 'px';
			if (width == '*') width = 'auto';
			colWidths[i] = width;
		}
	}

	items.each(function(){
		var el = $(this),
			title = el.attr('x-title'),
			noTitle = el.attr('x-show-title') == 'false';

		var labelScope = el.data('$scope');
		if (labelScope) {
			labelScope = labelScope.$new();
		}

		if (numCols > 1 && !noTitle && title) {
			var label = $('').html(title).attr('x-for-widget', el.attr('id')),
				labelElem = $compile(label)(labelScope || $scope);
			el.data('label', labelElem);
			return add(el, labelElem);
		}
		add(el);
	});

	var table = $('
</table');

	function isLabel(cell) {
		return cell.css === "form-label" || (cell.elem && cell.elem.is('label,.spacer-item'));
	}

	function computeWidths(row) {
		if (row.length === 1) return null;
		var widths = [],
			labelCols = 0,
			itemCols = 0,
			emptyCols = 0;

		_.each(row, function(cell) {
			if (isLabel(cell)) {
				labelCols += (cell.colspan || 1);
			} else {
				itemCols += (cell.colspan || 1);
			}
		});

		emptyCols = numCols - (labelCols + itemCols);

		labelCols += (emptyCols / 2);
		itemCols += (emptyCols / 2) + (emptyCols % 2);

		var labelWidth = labelCols ? Math.min(50, (12 * labelCols)) / labelCols : 0;
		var itemWidth = (100 - (labelWidth * labelCols)) / itemCols;

		_.each(row, function(cell, i) {
			var width = ((isLabel(cell) ? labelWidth : itemWidth) * (cell.colspan || 1));
			widths[i] = width + "%";
		});

		return widths;
	}

	_.each(layout, function(row){
		var tr = $('

'),
			numCells = 0,
			widths = colWidths || computeWidths(row);

		_.each(row, function(cell, i) {
				el = $('	')
					.addClass(cell.css)
					.attr('colspan', cell.colspan)
					.attr('rowspan', cell.rowspan)
					.append(cell.elem)
					.appendTo(tr);
				if (_.isArray(widths) && widths[i]) {
					el.width(widths[i]);
				}
				if ($(cell.elem).is('.form-item-container') && __appSettings['view.form.hot-edit']) {
					$(cell.elem).prepend($(''));
				}
				numCells += cell.colspan || 1;
		});

		// append remaining cells
		for (var i = numCells ; i < numCols ; i++) {
			$('	').appendTo(tr).width((widths||[])[i]);
		}

		tr.appendTo(table);
	});

	return table;
} //- TableLayout

ui.directive('uiTableLayout', ['$compile', function($compile) {

	return function(scope, element, attrs) {
		var elem = attrs.layoutSelector ? element.find(attrs.layoutSelector) : element;
		var items = elem.children();

		var layout = TableLayout(items, attrs, scope, $compile);
		var brTags = element.children('br:hidden'); // detach all the
 tags

		scope.$on('$destroy', function(){
			brTags.remove();
		});

		elem.append(layout);
	};

}]);

function PanelLayout(items, attrs, $scope, $compile) {

	var stacked = attrs.stacked || false,
		numCols = 12,
		numSpan = +(attrs.itemSpan) || 6,
		curCol = 0,
		layout = [$('
')];

	function add(item, label) {
		var row = _.last(layout),
			cell = $('
'),
			span = +item.attr('x-colspan') || numSpan,
			offset = +item.attr('x-coloffset') || 0;

		span = Math.min(span, numCols);
		if (stacked) {
			span = 0;
		}

		if (item.is('.spacer-item')) {
			curCol += (span + offset);
			item.remove();
			return;
		}

		if (curCol + (span + offset) >= numCols + 1 && !stacked) {
			curCol = 0, row = $('
');
			layout.push(row);
		}
		if (label) {
			label.appendTo(cell);
			row.addClass('has-labels');
		}

		cell.addClass(item.attr('x-cell-css'));

		if (span) {
			cell.addClass('span' + span);
		}
		if (offset) {
			cell.addClass('offset' + offset);
		}

		cell.append(item);
		cell.appendTo(row);

		curCol += (span + offset);
	}

	items.each(function (item, i) {
		var el = $(this),
			title = el.attr('x-title'),
			noTitle = el.attr('x-show-title') == 'false';

		var labelScope = el.data('$scope');
		if (labelScope) {
			labelScope = labelScope.$new();
		}

		if (!noTitle && title) {
			var label = $('').html(title).attr('x-for-widget', el.attr('id')),
				labelElem = $compile(label)(labelScope || $scope);
			el.data('label', labelElem);
			return add(el, labelElem);
		}
		add(el);
	});

	var container = $('
</div').append(layout);

	return container;
}

ui.directive('uiPanelLayout', ['$compile', function($compile) {

	return function(scope, element, attrs) {
		var elem = element.children('[ui-transclude]:first');
		var items = elem.children();
		var layout = PanelLayout(items, attrs, scope, $compile);
		elem.append(layout);
	};

}]);

function BarLayout(items, attrs, $scope, $compile) {

	var main = $('
');
	var side = $('
');

	items.each(function(item, i) {
		var elem = $(this);
		var prop = elem.scope().field || {};
		if (elem.attr('x-sidebar')) {
			elem.appendTo(side);
		} else {
			elem.appendTo(main);
		}
		if (prop.attached) {
			elem.addClass("attached");
		}
	});

	if (side.children().size() == 0) {
		main.removeClass("span8").addClass("span12");
		side = null;
	}

	var row = $('
').append(main);

	if (side) {
		side.appendTo(row);
	}

	return row;
}

ui.directive('uiBarLayout', ['$compile', function($compile) {

	return function(scope, element, attrs) {
		var items = element.children();
		var layout = BarLayout(items, attrs, scope, $compile);
		var schema = scope.schema || {};
		var css = null;

		scope._isPanelForm = true;

		element.append(layout);
		element.addClass('bar-layout');

		if (element.has('[x-sidebar]').size() === 0) {
			css = "mid";
		}
		if (element.is('form') && ["mini", "mid", "large"].indexOf(schema.width) > -1) {
			css = scope.schema.width;
		}
		if (css) {
			element.addClass(css + '-form');
		}
	};
}]);

ui.directive('uiPanelEditor', ['$compile', function($compile) {

	return {
		scope: true,
		link: function(scope, element, attrs) {
			var field = scope.field;
			var editor = field.editor;

			if (!editor) {
				return;
			}

			var items = editor.items || [];
			var widths = _.map(items, function (item) {
				if (!item.showTitle) {
					item.placeholder = item.placeholder || item.title || item.autoTitle;
				}
				item.showTitle = item.showTitle === true;
				var width = item.width || (item.widgetAttrs||{}).width;
				return width ? width : (item.widget === 'toggle' ? 24 : '*');
			});

			var schema = {
				cols: items.length,
				colWidths: widths.join(','),
				items: items
			};

			if (editor.layout !== 'table') {
				schema = {
					items: [{
						type: 'panel',
						items: items
					}]
				};
			}

			scope.fields = editor.fields || scope.fields;

			var form = ui.formBuild(scope, schema, scope.fields);
			var isRelational = /-to-one$/.test(field.type);

			if (isRelational) {
				Object.defineProperty(scope, 'record', {
					enumerable: true,
					get: function () {
						return (scope.$parent.record||{})[field.name];
					},
					set: function (value) {
						scope.setValue(value, true);
					}
				});
				Object.defineProperty(scope, '$$original', {
					enumerable: true,
					get: function () {
						return (scope.$parent.$$original||{})[field.name];
					},
					set: function (value) {}
				});
				scope.getContext = function () {
					var context = _.extend({}, scope.record);
					context._model = scope._model;
					return context;
				};
				// make sure to fetch missing values
				scope.$watch('record.id', function (value, old) {
					var ds = scope._dataSource;
					var record = scope.record;
					if (value <= 0 || !value || record.$fetched || record.$fetchedRelated) {
						return;
					}
					var missing = _.filter(_.keys(editor.fields), function (name) {
						return !record.hasOwnProperty(name);
					});
					if (missing.length === 0) {
						return;
					}
					record.$fetchedRelated = true;
					return ds.read(value.id).success(function(rec) {
						var values = _.pick(rec, missing);
						record = _.extend(record, values);
					});
				});
			}

			form = $compile(form)(scope);
			form.children('div.row').removeClass('row').addClass('row-fluid');
			element.append(form);

			scope.$watch('form.$valid', function (valid, old) {
				if (isRelational && editor.showOnNew === false && !scope.canShowEditor()) {
					return;
				}
				if (scope.setValidity) {
					scope.setValidity('valid', valid);
				}
			});

			scope.$on('$destroy', function () {
				if (scope.setValidity) {
					scope.setValidity('valid', true);
				}
			});
		}
	};
}]);

})(this);
<commit_msg>Fix field editor validation issue<commit_after>/*
 * Axelor Business Solutions
 *
 * Copyright (C) 2005-2014 Axelor (<http://axelor.com>).
 *
 * This program is free software: you can redistribute it and/or  modify
 * it under the terms of the GNU Affero General Public License, version 3,
 * as published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
(function() {

var ui = angular.module('axelor.ui');

function TableLayout(items, attrs, $scope, $compile) {

	var colWidths = attrs.widths,
		numCols = +attrs.cols || 4,
		curCol = 0,
		layout = [[]];

	function add(item, label) {

		if (item.is('br')) {
			curCol = 0;
			item.hide();
			return layout.push([]);
		}

		var row = _.last(layout),
			cell = null,
			colspan = +item.attr('x-colspan') || 1,
			rowspan = +item.attr('x-rowspan') || 1;

		if (curCol + colspan >= numCols + 1) {
			curCol = 0, row = [];
			layout.push(row);
		}

		if (label) {
			cell = {};
			cell.elem = label;
			cell.css = label.attr('x-cell-css');
			row.push(cell);
			if (rowspan > 1) cell.rowspan = rowspan;
			if (colspan > 1) colspan -= 1;
			curCol += 1;
		}

		cell = {};
		cell.elem = item;
		cell.css = item.attr('x-cell-css');
		if (colspan > 1) cell.colspan = colspan;
		if (rowspan > 1) cell.rowspan = rowspan;

		row.push(cell);
		curCol += colspan;
	}

	if (colWidths && angular.isString(colWidths)) {
		colWidths = colWidths.trim().split(/\s*,\s*/);
		for(var i = 0 ; i < colWidths.length; i++) {
			var width = colWidths[i];
			if (/^(\d+)$/.test(width)) width = width + 'px';
			if (width == '*') width = 'auto';
			colWidths[i] = width;
		}
	}

	items.each(function(){
		var el = $(this),
			title = el.attr('x-title'),
			noTitle = el.attr('x-show-title') == 'false';

		var labelScope = el.data('$scope');
		if (labelScope) {
			labelScope = labelScope.$new();
		}

		if (numCols > 1 && !noTitle && title) {
			var label = $('').html(title).attr('x-for-widget', el.attr('id')),
				labelElem = $compile(label)(labelScope || $scope);
			el.data('label', labelElem);
			return add(el, labelElem);
		}
		add(el);
	});

	var table = $('
</table');

	function isLabel(cell) {
		return cell.css === "form-label" || (cell.elem && cell.elem.is('label,.spacer-item'));
	}

	function computeWidths(row) {
		if (row.length === 1) return null;
		var widths = [],
			labelCols = 0,
			itemCols = 0,
			emptyCols = 0;

		_.each(row, function(cell) {
			if (isLabel(cell)) {
				labelCols += (cell.colspan || 1);
			} else {
				itemCols += (cell.colspan || 1);
			}
		});

		emptyCols = numCols - (labelCols + itemCols);

		labelCols += (emptyCols / 2);
		itemCols += (emptyCols / 2) + (emptyCols % 2);

		var labelWidth = labelCols ? Math.min(50, (12 * labelCols)) / labelCols : 0;
		var itemWidth = (100 - (labelWidth * labelCols)) / itemCols;

		_.each(row, function(cell, i) {
			var width = ((isLabel(cell) ? labelWidth : itemWidth) * (cell.colspan || 1));
			widths[i] = width + "%";
		});

		return widths;
	}

	_.each(layout, function(row){
		var tr = $('

'),
			numCells = 0,
			widths = colWidths || computeWidths(row);

		_.each(row, function(cell, i) {
				el = $('	')
					.addClass(cell.css)
					.attr('colspan', cell.colspan)
					.attr('rowspan', cell.rowspan)
					.append(cell.elem)
					.appendTo(tr);
				if (_.isArray(widths) && widths[i]) {
					el.width(widths[i]);
				}
				if ($(cell.elem).is('.form-item-container') && __appSettings['view.form.hot-edit']) {
					$(cell.elem).prepend($(''));
				}
				numCells += cell.colspan || 1;
		});

		// append remaining cells
		for (var i = numCells ; i < numCols ; i++) {
			$('	').appendTo(tr).width((widths||[])[i]);
		}

		tr.appendTo(table);
	});

	return table;
} //- TableLayout

ui.directive('uiTableLayout', ['$compile', function($compile) {

	return function(scope, element, attrs) {
		var elem = attrs.layoutSelector ? element.find(attrs.layoutSelector) : element;
		var items = elem.children();

		var layout = TableLayout(items, attrs, scope, $compile);
		var brTags = element.children('br:hidden'); // detach all the
 tags

		scope.$on('$destroy', function(){
			brTags.remove();
		});

		elem.append(layout);
	};

}]);

function PanelLayout(items, attrs, $scope, $compile) {

	var stacked = attrs.stacked || false,
		numCols = 12,
		numSpan = +(attrs.itemSpan) || 6,
		curCol = 0,
		layout = [$('
')];

	function add(item, label) {
		var row = _.last(layout),
			cell = $('
'),
			span = +item.attr('x-colspan') || numSpan,
			offset = +item.attr('x-coloffset') || 0;

		span = Math.min(span, numCols);
		if (stacked) {
			span = 0;
		}

		if (item.is('.spacer-item')) {
			curCol += (span + offset);
			item.remove();
			return;
		}

		if (curCol + (span + offset) >= numCols + 1 && !stacked) {
			curCol = 0, row = $('
');
			layout.push(row);
		}
		if (label) {
			label.appendTo(cell);
			row.addClass('has-labels');
		}

		cell.addClass(item.attr('x-cell-css'));

		if (span) {
			cell.addClass('span' + span);
		}
		if (offset) {
			cell.addClass('offset' + offset);
		}

		cell.append(item);
		cell.appendTo(row);

		curCol += (span + offset);
	}

	items.each(function (item, i) {
		var el = $(this),
			title = el.attr('x-title'),
			noTitle = el.attr('x-show-title') == 'false';

		var labelScope = el.data('$scope');
		if (labelScope) {
			labelScope = labelScope.$new();
		}

		if (!noTitle && title) {
			var label = $('').html(title).attr('x-for-widget', el.attr('id')),
				labelElem = $compile(label)(labelScope || $scope);
			el.data('label', labelElem);
			return add(el, labelElem);
		}
		add(el);
	});

	var container = $('
</div').append(layout);

	return container;
}

ui.directive('uiPanelLayout', ['$compile', function($compile) {

	return function(scope, element, attrs) {
		var elem = element.children('[ui-transclude]:first');
		var items = elem.children();
		var layout = PanelLayout(items, attrs, scope, $compile);
		elem.append(layout);
	};

}]);

function BarLayout(items, attrs, $scope, $compile) {

	var main = $('
');
	var side = $('
');

	items.each(function(item, i) {
		var elem = $(this);
		var prop = elem.scope().field || {};
		if (elem.attr('x-sidebar')) {
			elem.appendTo(side);
		} else {
			elem.appendTo(main);
		}
		if (prop.attached) {
			elem.addClass("attached");
		}
	});

	if (side.children().size() == 0) {
		main.removeClass("span8").addClass("span12");
		side = null;
	}

	var row = $('
').append(main);

	if (side) {
		side.appendTo(row);
	}

	return row;
}

ui.directive('uiBarLayout', ['$compile', function($compile) {

	return function(scope, element, attrs) {
		var items = element.children();
		var layout = BarLayout(items, attrs, scope, $compile);
		var schema = scope.schema || {};
		var css = null;

		scope._isPanelForm = true;

		element.append(layout);
		element.addClass('bar-layout');

		if (element.has('[x-sidebar]').size() === 0) {
			css = "mid";
		}
		if (element.is('form') && ["mini", "mid", "large"].indexOf(schema.width) > -1) {
			css = scope.schema.width;
		}
		if (css) {
			element.addClass(css + '-form');
		}
	};
}]);

ui.directive('uiPanelEditor', ['$compile', function($compile) {

	return {
		scope: true,
		link: function(scope, element, attrs) {
			var field = scope.field;
			var editor = field.editor;

			if (!editor) {
				return;
			}

			var items = editor.items || [];
			var widths = _.map(items, function (item) {
				if (!item.showTitle) {
					item.placeholder = item.placeholder || item.title || item.autoTitle;
				}
				item.showTitle = item.showTitle === true;
				var width = item.width || (item.widgetAttrs||{}).width;
				return width ? width : (item.widget === 'toggle' ? 24 : '*');
			});

			var schema = {
				cols: items.length,
				colWidths: widths.join(','),
				items: items
			};

			if (editor.layout !== 'table') {
				schema = {
					items: [{
						type: 'panel',
						items: items
					}]
				};
			}

			scope.fields = editor.fields || scope.fields;

			var form = ui.formBuild(scope, schema, scope.fields);
			var isRelational = /-to-one$/.test(field.type);

			if (isRelational) {
				Object.defineProperty(scope, 'record', {
					enumerable: true,
					get: function () {
						return (scope.$parent.record||{})[field.name];
					},
					set: function (value) {
						scope.setValue(value, true);
					}
				});
				Object.defineProperty(scope, '$$original', {
					enumerable: true,
					get: function () {
						return (scope.$parent.$$original||{})[field.name];
					},
					set: function (value) {}
				});
				scope.getContext = function () {
					var context = _.extend({}, scope.record);
					context._model = scope._model;
					return context;
				};
				// make sure to fetch missing values
				scope.$watch('record.id', function (value, old) {
					var ds = scope._dataSource;
					var record = scope.record;
					if (value <= 0 || !value || record.$fetched || record.$fetchedRelated) {
						return;
					}
					var missing = _.filter(_.keys(editor.fields), function (name) {
						return !record.hasOwnProperty(name);
					});
					if (missing.length === 0) {
						return;
					}
					record.$fetchedRelated = true;
					return ds.read(value.id).success(function(rec) {
						var values = _.pick(rec, missing);
						record = _.extend(record, values);
					});
				});

				// make sure to trigger record-change with proper record data
				scope.$watch('record', function (value, old) {
					scope.$broadcast("on:record-change", value);
				}, true);
				scope.$timeout(function () {
					scope.$broadcast("on:record-change", scope.record);
				});
			}

			form = $compile(form)(scope);
			form.children('div.row').removeClass('row').addClass('row-fluid');
			element.append(form);

			scope.isValid = function () {
				return scope.form && scope.form.$valid;
			};

			scope.$watch(function () {
				if (isRelational && editor.showOnNew === false && !scope.canShowEditor()) {
					return;
				}
				var valid = scope.isValid();
				if (scope.setValidity) {
					scope.setValidity('valid', valid);
				} else {
					scope.$parent.form.$setValidity('valid', valid, scope.form);
				}
			});

			scope.$on('$destroy', function () {
				if (scope.setValidity) {
					scope.setValidity('valid', true);
				}
			});
		}
	};
}]);

})(this);
<|endoftext|>


<commit_before>import React from 'react';
import * as activityFns from './activityfns.js';
import ActivityDay from './activityday.js';
import '../../styles/activities.css';
import * as DB from '../../constants/database.js';
import app from '../Firebase';

const db = app.firestore();

/**
 * Gets the list of activities from the server.
 *
 * @param {string} tripId The trip ID.
 */
export async function getActivityList(tripId) {
  return new Promise(function(resolve, reject) {
    let tripActivities = [];

    db.collection(DB.COLLECTION_TRIPS).doc(tripId)
    .collection(DB.COLLECTION_ACTIVITIES).get()
    .then(querySnapshot => {
      querySnapshot.forEach(doc => {
        let data = doc.data();
        data['id'] = doc.id;
        data['tripId'] = tripId;

        // TODO: if start date != end date, split into 2 days. (#37)

        // Eliminate nanoseconds, convert to milliseconds.
        data[DB.ACTIVITIES_START_TIME] =
          data[DB.ACTIVITIES_START_TIME]['seconds'] * 1000;
        data[DB.ACTIVITIES_END_TIME] =
          data[DB.ACTIVITIES_END_TIME]['seconds'] * 1000;

        tripActivities.push(data);
      })
    }).catch(error => {
      console.log('It seems that an error has occured.');
      tripActivities = null;
    }).then( () => resolve(tripActivities) );
  })
}

/**
 * The list of activities.
 *
 * @param {Object} props This component expects the following props:
 * - `tripId` {string} The trip's ID.
 */
class ActivityList extends React.Component {
  /** @inheritdoc */
  constructor(props) {
    super(props);
    this.state = { days : [] };
  }

  /**
   * @inheritdoc
   *
   * Get sorted list of activities from the database.
   *
   * This function only queries the database if `this.state` is defined.
   * `this.state` gains an entry with key `days` with the list of activities
   * sorted by date.
   *
   * This function also expects `this.props.tripId` to exist.
   */
  async componentDidMount() {
    if (this.state === null || this.props.tripId === null) { return; }
    let tripActivities = await getActivityList(this.props.tripId);
    if (tripActivities === null) {
      this.setState({days: null});
      return;
    }
    this.setState({days: activityFns.sortByDate(tripActivities)});
  }

  /** @inheritdoc */
  render() {
    if (this.state === null) { return (

); }
    if (this.state.days === null) {
      return (
An error has occurred :(
 );
    } else if (this.state.days.length === 0) {
      return (
Plan your trip here!
);
    }
    return (

        {this.state.days.map((item, index) => (
          <ActivityDay date={item[0]} activities={item[1]} key={index}/>
        ))}

    );
  }
}

export default ActivityList;
<commit_msg>more comprehensive error statements<commit_after>import React from 'react';
import * as activityFns from './activityfns.js';
import ActivityDay from './activityday.js';
import '../../styles/activities.css';
import * as DB from '../../constants/database.js';
import app from '../Firebase';

const db = app.firestore();

/**
 * Gets the list of activities from the server.
 *
 * @param {string} tripId The trip ID.
 */
export async function getActivityList(tripId) {
  return new Promise(function(resolve, reject) {
    let tripActivities = [];

    db.collection(DB.COLLECTION_TRIPS).doc(tripId)
    .collection(DB.COLLECTION_ACTIVITIES).get()
    .then(querySnapshot => {
      querySnapshot.forEach(doc => {
        let data = doc.data();
        data['id'] = doc.id;
        data['tripId'] = tripId;

        // TODO: if start date != end date, split into 2 days. (#37)

        // Eliminate nanoseconds, convert to milliseconds.
        data[DB.ACTIVITIES_START_TIME] =
          data[DB.ACTIVITIES_START_TIME]['seconds'] * 1000;
        data[DB.ACTIVITIES_END_TIME] =
          data[DB.ACTIVITIES_END_TIME]['seconds'] * 1000;

        tripActivities.push(data);
      })
    }).catch(error => {
      console.log('It seems that an error has occured.', error);
      tripActivities = null;
    }).then( () => resolve(tripActivities) );
  })
}

/**
 * The list of activities.
 *
 * @param {Object} props This component expects the following props:
 * - `tripId` {string} The trip's ID.
 */
class ActivityList extends React.Component {
  /** @inheritdoc */
  constructor(props) {
    super(props);
    this.state = { days : [] };
  }

  /**
   * @inheritdoc
   *
   * Get sorted list of activities from the database.
   *
   * This function only queries the database if `this.state` is defined.
   * `this.state` gains an entry with key `days` with the list of activities
   * sorted by date.
   *
   * This function also expects `this.props.tripId` to exist.
   */
  async componentDidMount() {
    if (this.state === null || this.props.tripId === null) { return; }
    let tripActivities = await getActivityList(this.props.tripId);
    if (tripActivities === null) {
      this.setState({days: null});
      return;
    }
    this.setState({days: activityFns.sortByDate(tripActivities)});
  }

  /** @inheritdoc */
  render() {
    if (this.state === null) { return (

); }
    if (this.state.days === null) {
      return (
An error has occurred :(
 );
    } else if (this.state.days.length === 0) {
      return (
Plan your trip here!
);
    }
    return (

        {this.state.days.map((item, index) => (
          <ActivityDay date={item[0]} activities={item[1]} key={index}/>
        ))}

    );
  }
}

export default ActivityList;
<|endoftext|>


<commit_before>(function () {
    "use strict";

    function PublishController($scope, localizationService) {

        var vm = this;
        vm.variants = $scope.model.variants;
        vm.changeSelection = changeSelection;
        vm.loading = true;
        vm.dirtyVariantFilter = dirtyVariantFilter;
        vm.pristineVariantFilter = pristineVariantFilter;
        vm.hasPristineVariants = false;

        //watch this model, if it's reset, then re init
        $scope.$watch("model.variants",
            function (newVal, oldVal) {
                vm.variants = newVal;
                if (oldVal && oldVal.length) {
                    //re-bind the selections
                    for (var i = 0; i < oldVal.length; i++) {
                        var found = _.find(vm.variants, function (v) {
                            return v.language.id === oldVal[i].language.id;
                        });
                        if (found) {
                            found.publish = oldVal[i].publish;
                        }
                    }
                }
                onInit();
            });

        function changeSelection(variant) {
            var firstSelected = _.find(vm.variants, function (v) {
                return v.publish;
            });
            $scope.model.disableSubmitButton = !firstSelected; //disable submit button if there is none selected
        }

        function dirtyVariantFilter(variant) {
            //determine a variant is 'dirty' (meaning it will show up as publish-able) if it's
            // * the active one
            // * it's editor is in a $dirty state
            // * it has pending saves
            // * it is unpublished
            return (variant.active || variant.isDirty || variant.isEdited === true || (variant.isEdited === false && variant.state === "Unpublished"));
        }

        function pristineVariantFilter(variant) {
            return !(dirtyVariantFilter(variant));
        }

        function onInit() {

            if(!$scope.model.title) {
                localizationService.localize("content_readyToPublish").then(function(value){
                    $scope.model.title = value;
                });
            }

            vm.hasPristineVariants = false;

            _.each(vm.variants,
                function (variant) {
                    variant.compositeId = variant.language.culture + "_" + (variant.segment ? variant.segment : "");
                    variant.htmlId = "publish_variant_" + variant.compositeId;

                    //check for pristine variants
                    if (!vm.hasPristineVariants) {
                        vm.hasPristineVariants = pristineVariantFilter(variant);
                    }
                });

            if (vm.variants.length !== 0) {
                //now sort it so that the current one is at the top
                vm.variants = _.sortBy(vm.variants, function (v) {
                    return v.current ? 0 : 1;
                });

                var active = _.find(vm.variants, function (v) {
                    return v.active;
                });

                if (active) {
                    //ensure that the current one is selected
                    active.publish = true;
                }

            } else {
                //disable Publish button if we have nothing to publish
                $scope.model.disableSubmitButton = true;
            }

            vm.loading = false;
        }
    }

    angular.module("umbraco").controller("Umbraco.Overlays.PublishController", PublishController);

})();
<commit_msg>fix sort issue on publish dialog<commit_after>(function () {
    "use strict";

    function PublishController($scope, localizationService) {

        var vm = this;
        vm.variants = $scope.model.variants;
        vm.changeSelection = changeSelection;
        vm.loading = true;
        vm.dirtyVariantFilter = dirtyVariantFilter;
        vm.pristineVariantFilter = pristineVariantFilter;
        vm.hasPristineVariants = false;

        //watch this model, if it's reset, then re init
        $scope.$watch("model.variants",
            function (newVal, oldVal) {
                vm.variants = newVal;
                if (oldVal && oldVal.length) {
                    //re-bind the selections
                    for (var i = 0; i < oldVal.length; i++) {
                        var found = _.find(vm.variants, function (v) {
                            return v.language.id === oldVal[i].language.id;
                        });
                        if (found) {
                            found.publish = oldVal[i].publish;
                        }
                    }
                }
                onInit();
            });

        function changeSelection(variant) {
            var firstSelected = _.find(vm.variants, function (v) {
                return v.publish;
            });
            $scope.model.disableSubmitButton = !firstSelected; //disable submit button if there is none selected
        }

        function dirtyVariantFilter(variant) {
            //determine a variant is 'dirty' (meaning it will show up as publish-able) if it's
            // * the active one
            // * it's editor is in a $dirty state
            // * it has pending saves
            // * it is unpublished
            return (variant.active || variant.isDirty || variant.isEdited === true || (variant.isEdited === false && variant.state === "Unpublished"));
        }

        function pristineVariantFilter(variant) {
            return !(dirtyVariantFilter(variant));
        }

        function onInit() {

            if(!$scope.model.title) {
                localizationService.localize("content_readyToPublish").then(function(value){
                    $scope.model.title = value;
                });
            }

            vm.hasPristineVariants = false;

            _.each(vm.variants,
                function (variant) {
                    variant.compositeId = variant.language.culture + "_" + (variant.segment ? variant.segment : "");
                    variant.htmlId = "publish_variant_" + variant.compositeId;

                    //check for pristine variants
                    if (!vm.hasPristineVariants) {
                        vm.hasPristineVariants = pristineVariantFilter(variant);
                    }
                });

            if (vm.variants.length !== 0) {
                //now sort it so that the current one is at the top
                vm.variants = _.sortBy(vm.variants, function (v) {
                    return v.active ? 0 : 1;
                });

                var active = _.find(vm.variants, function (v) {
                    return v.active;
                });

                if (active) {
                    //ensure that the current one is selected
                    active.publish = true;
                }

            } else {
                //disable Publish button if we have nothing to publish
                $scope.model.disableSubmitButton = true;
            }

            vm.loading = false;
        }
    }

    angular.module("umbraco").controller("Umbraco.Overlays.PublishController", PublishController);

})();
<|endoftext|>


<commit_before>/**
 * Created by sam on 13.11.2016.
 */

const spawnSync = require('child_process').spawnSync;
const path = require('path');
const fs = require('fs');

function gitHead(cwd) {
  const r = spawnSync('git', ['rev-parse', '--verify', 'HEAD'], {
    cwd: cwd
  });
  if (!r.stdout) {
    console.error(cwd, r.error);
    return 'error';
  }
  return r.stdout.toString().trim();
}

function resolvePlugin(repo, version) {
  if (fs.lstatSync('.git').isDirectory() && repo) {
    if (repo.endsWith('.git')) {
      repo = repo.slice(0, repo.length - 4);
      return repo + '/commit/' + gitHead('.');
    }
  }
  // not a git repo
  return version;
}

function toVersion(v) {
  const now = new Date().toISOString();
  // %Y%m%d-%H%M%S
  const fmt = now
    .replace(/T/, ' ')
    .replace(/\..+/, '')
    .replace(/[-:]/, '')
    .replace(' ', '-');
  return v.replace('SNAPSHOT', fmt);
}

function _main() {
  const pkg = require('./package.json');
  const name = pkg.name;
  const version = toVersion(pkg.version);
  const resolved = resolvePlugin((pkg.repository || {}).url, version);

  const buildInfo = {
    name,
    version,
    resolved,
    description: pkg.description,
    homepage: pkg.homepage,
    repository: (pkg.repository || {}).url
  };

  const l = ('build/source/' + name).split('/');
  l.forEach((_, i) => {
    const path = l.slice(0, i + 1).join('/');
    if (!fs.existsSync(path)) {
      fs.mkdirSync(path);
    }
  });

  fs.writeFileSync('build/source/' + name + '/buildInfo.json', JSON.stringify(buildInfo, null, ' '));
}

if (require.main === module) {
  _main();
}
<commit_msg>fix linting<commit_after>/**
 * Created by sam on 13.11.2016.
 */

const spawnSync = require('child_process').spawnSync;
const fs = require('fs');

function gitHead(cwd) {
  const r = spawnSync('git', ['rev-parse', '--verify', 'HEAD'], {
    cwd: cwd
  });
  if (!r.stdout) {
    console.error(cwd, r.error);
    return 'error';
  }
  return r.stdout.toString().trim();
}

function resolvePlugin(repo, version) {
  if (fs.lstatSync('.git').isDirectory() && repo) {
    if (repo.endsWith('.git')) {
      repo = repo.slice(0, repo.length - 4);
      return repo + '/commit/' + gitHead('.');
    }
  }
  // not a git repo
  return version;
}

function toVersion(v) {
  const now = new Date().toISOString();
  // %Y%m%d-%H%M%S
  const fmt = now
    .replace(/T/, ' ')
    .replace(/\..+/, '')
    .replace(/[-:]/, '')
    .replace(' ', '-');
  return v.replace('SNAPSHOT', fmt);
}

function _main() {
  const pkg = require('./package.json');
  const name = pkg.name;
  const version = toVersion(pkg.version);
  const resolved = resolvePlugin((pkg.repository || {}).url, version);

  const buildInfo = {
    name,
    version,
    resolved,
    description: pkg.description,
    homepage: pkg.homepage,
    repository: (pkg.repository || {}).url
  };

  const l = ('build/source/' + name).split('/');
  l.forEach((_, i) => {
    const path = l.slice(0, i + 1).join('/');
    if (!fs.existsSync(path)) {
      fs.mkdirSync(path);
    }
  });

  fs.writeFileSync('build/source/' + name + '/buildInfo.json', JSON.stringify(buildInfo, null, ' '));
}

if (require.main === module) {
  _main();
}
<|endoftext|>


<commit_before>const errors = require('@tryghost/errors');
const DomainEvents = require('@tryghost/domain-events');
const {MemberSubscribeEvent} = require('@tryghost/member-events');

const messages = {
    emailVerificationNeeded: `We're hard at work processing your import. To make sure you get great deliverability on a list of that size, we'll need to enable some extra features for your account. A member of our team will be in touch with you by email to review your account make sure everything is configured correctly so you're ready to go.`,
    emailVerificationEmailSubject: `Email needs verification`,
    emailVerificationEmailMessage: `Email verification needed for site: {siteUrl}, just imported: {importedNumber} members.`
};

class VerificationTrigger {
    /**
     *
     * @param {object} deps
     * @param {number} deps.configThreshold Threshold for triggering verification as defined in config
     * @param {() => boolean} deps.isVerified Check Ghost config to see if we are already verified
     * @param {() => boolean} deps.isVerificationRequired Check Ghost settings to see whether verification has been requested
     * @param {(content: {subject: string, message: string, amountImported: number}) => {}} deps.sendVerificationEmail Sends an email to the escalation address to confirm that customer needs to be verified
     * @param {any} deps.membersStats MemberStats service
     * @param {any} deps.Settings Ghost Settings model
     * @param {any} deps.eventRepository For querying events
     */
    constructor({
        configThreshold,
        isVerified,
        isVerificationRequired,
        sendVerificationEmail,
        membersStats,
        Settings,
        eventRepository
    }) {
        this._configThreshold = configThreshold;
        this._isVerified = isVerified;
        this._isVerificationRequired = isVerificationRequired;
        this._sendVerificationEmail = sendVerificationEmail;
        this._membersStats = membersStats;
        this._Settings = Settings;
        this._eventRepository = eventRepository;

        DomainEvents.subscribe(MemberSubscribeEvent, async (event) => {
            if (event.data.source === 'api' && isFinite(this._configThreshold)) {
                const createdAt = new Date();
                createdAt.setDate(createdAt.getDate() - 30);
                const events = await this._eventRepository.getNewsletterSubscriptionEvents({}, {
                    'data.source': 'data.source:api',
                    'data.created_at': `data.created_at:>${createdAt.toISOString().replace('T', ' ').substring(0, 19)}`
                });

                if (events.meta.pagination.total > this._configThreshold) {
                    await this.startVerificationProcess({
                        amountImported: events.meta.pagination.total,
                        throwOnTrigger: false
                    });
                }
            }
        });
    }

    async getImportThreshold() {
        const volumeThreshold = this._configThreshold;
        if (isFinite(volumeThreshold)) {
            const membersTotal = await this._membersStats.getTotalMembers();
            return Math.max(membersTotal, volumeThreshold);
        } else {
            return volumeThreshold;
        }
    }

    /** @typedef IVerificationResult
     * @property {boolean} needsVerification Whether the verification workflow was triggered
     */

    /**
     *
     * @param {object} config
     * @param {number} config.amountImported Amount of members which were imported
     * @param {boolean} config.throwOnTrigger Whether to throw if verification is needed
     * @returns {Promise<IVerificationResult>} Object containing property "needsVerification" - true when triggered
     */
    async startVerificationProcess({
        amountImported,
        throwOnTrigger
    }) {
        if (!this._isVerified()) {
            // Only trigger flag change and escalation email the first time
            if (!this._isVerificationRequired()) {
                await this._Settings.edit([{
                    key: 'email_verification_required',
                    value: true
                }], {context: {internal: true}});

                this._sendVerificationEmail({
                    message: messages.emailVerificationEmailMessage,
                    subject: messages.emailVerificationEmailSubject,
                    amountImported
                });

                if (throwOnTrigger) {
                    throw new errors.ValidationError({
                        message: messages.emailVerificationNeeded
                    });
                }

                return {
                    needsVerification: true
                };
            }
        }

        return {
            needsVerification: false
        };
    }
}

module.exports = VerificationTrigger;
<commit_msg>Add API source when verification triggered from API events<commit_after>const errors = require('@tryghost/errors');
const DomainEvents = require('@tryghost/domain-events');
const {MemberSubscribeEvent} = require('@tryghost/member-events');

const messages = {
    emailVerificationNeeded: `We're hard at work processing your import. To make sure you get great deliverability on a list of that size, we'll need to enable some extra features for your account. A member of our team will be in touch with you by email to review your account make sure everything is configured correctly so you're ready to go.`,
    emailVerificationEmailSubject: `Email needs verification`,
    emailVerificationEmailMessageImport: `Email verification needed for site: {siteUrl}, just imported: {importedNumber} members.`,
    emailVerificationEmailMessageAPI: `Email verification needed for site: {siteUrl} has added: {importedNumber} members through the API in the last 30 days.`
};

class VerificationTrigger {
    /**
     *
     * @param {object} deps
     * @param {number} deps.configThreshold Threshold for triggering verification as defined in config
     * @param {() => boolean} deps.isVerified Check Ghost config to see if we are already verified
     * @param {() => boolean} deps.isVerificationRequired Check Ghost settings to see whether verification has been requested
     * @param {(content: {subject: string, message: string, amountImported: number}) => {}} deps.sendVerificationEmail Sends an email to the escalation address to confirm that customer needs to be verified
     * @param {any} deps.membersStats MemberStats service
     * @param {any} deps.Settings Ghost Settings model
     * @param {any} deps.eventRepository For querying events
     */
    constructor({
        configThreshold,
        isVerified,
        isVerificationRequired,
        sendVerificationEmail,
        membersStats,
        Settings,
        eventRepository
    }) {
        this._configThreshold = configThreshold;
        this._isVerified = isVerified;
        this._isVerificationRequired = isVerificationRequired;
        this._sendVerificationEmail = sendVerificationEmail;
        this._membersStats = membersStats;
        this._Settings = Settings;
        this._eventRepository = eventRepository;

        DomainEvents.subscribe(MemberSubscribeEvent, async (event) => {
            if (event.data.source === 'api' && isFinite(this._configThreshold)) {
                const createdAt = new Date();
                createdAt.setDate(createdAt.getDate() - 30);
                const events = await this._eventRepository.getNewsletterSubscriptionEvents({}, {
                    'data.source': 'data.source:api',
                    'data.created_at': `data.created_at:>${createdAt.toISOString().replace('T', ' ').substring(0, 19)}`
                });

                if (events.meta.pagination.total > this._configThreshold) {
                    await this.startVerificationProcess({
                        amountImported: events.meta.pagination.total,
                        throwOnTrigger: false,
                        source: 'api'
                    });
                }
            }
        });
    }

    async getImportThreshold() {
        const volumeThreshold = this._configThreshold;
        if (isFinite(volumeThreshold)) {
            const membersTotal = await this._membersStats.getTotalMembers();
            return Math.max(membersTotal, volumeThreshold);
        } else {
            return volumeThreshold;
        }
    }

    /** @typedef IVerificationResult
     * @property {boolean} needsVerification Whether the verification workflow was triggered
     */

    /**
     *
     * @param {object} config
     * @param {number} config.amountImported Amount of members which were imported
     * @param {boolean} config.throwOnTrigger Whether to throw if verification is needed
     * @param {string} config.source Source of the verification trigger - currently either 'api' or 'import'
     * @returns {Promise<IVerificationResult>} Object containing property "needsVerification" - true when triggered
     */
    async startVerificationProcess({
        amountImported,
        throwOnTrigger,
        source
    }) {
        if (!this._isVerified()) {
            // Only trigger flag change and escalation email the first time
            if (!this._isVerificationRequired()) {
                await this._Settings.edit([{
                    key: 'email_verification_required',
                    value: true
                }], {context: {internal: true}});

                this._sendVerificationEmail({
                    message: source === 'api'
                        ? messages.emailVerificationEmailMessageAPI
                        : messages.emailVerificationEmailMessageImport,
                    subject: messages.emailVerificationEmailSubject,
                    amountImported
                });

                if (throwOnTrigger) {
                    throw new errors.ValidationError({
                        message: messages.emailVerificationNeeded
                    });
                }

                return {
                    needsVerification: true
                };
            }
        }

        return {
            needsVerification: false
        };
    }
}

module.exports = VerificationTrigger;<|endoftext|>


<commit_before>define( 'providers/colabs',
[ 'providers', 'jquery', 'gizmo/superdesk',

  config.guiJs('livedesk', 'models/blog'),
  config.guiJs('livedesk', 'models/collaborator'),

  'providers/colabs/adaptor',
  'tmpl!livedesk>providers/colabs',
  'tmpl!livedesk>providers/colabs/items' ],

function(providers, $, giz, Blog, Collaborator)
{
    var config = { updateInterval: 10 },
        colabsList = [],
        updateInterval = 0,
        intervalRunning = false,
        updateItemCount = 0;

    // single post item view
    var PostView = giz.View.extend
    ({
        init: function()
        {
            var self = this;
            this.model.on('update', function(){ self.render(true); });
        },
        render: function(update)
        {
            var self = this;
            $.tmpl( 'livedesk>providers/colabs/items', {Posts: this.model.feed('json')}, function(e, o)
            {
                if(update) $(self.el).replaceWith($(o));
                else self.el = $(o);

                // make draggable
                self.el.hasClass('draggable') && self.el.draggable
                ({
                    helper: 'clone',
                    appendTo: 'body',
                    zIndex: 2700,
                    start: function(){ $(this).data('post', providers.colabs.adaptor.universal(this)); }
                });
            });
            return self;
        }
    }),
    // main view
    ColabView = giz.View.extend
    ({
        namespace: 'livedesk',
        events:
        {
            '.collaborators-header .feed-info .label': {'click': 'toggleHeader'},
            '.new-results': {'update': 'showNewResults'}
        },

        toggleHeader: function()
        {
            var colabId = $(this).attr('data-colab-id'),
                posts = $(this).nextAll('.search-result-list').find('li[data-colab-id='+colabId+']');

            if($(this).data('is-off'))
            {
                posts.show();
                $(this).addClass('label-info').data('is-off', false);
            }
            else
            {
                posts.hide();
                $(this).removeClass('label-info').data('is-off', true);
            }
        },
        /*!
         * @param e event
         * @param count
         * @param callback
         * @param wether to do update auto. or add a click-to-show button
         */
        showNewResults: function(e, count, callback, auto)
        {
            var self = $('.new-results', this.el),
                cb = function()
                {
                    self.addClass('hide');
                    callback.apply(this);
                };
            auto ? cb() : self.removeClass('hide').text( count+" "+_('new items! Update') ).one('click.livedesk', cb);
        },

        init: function()
        {
            $('.search-result-list', this.el).html('');
            var blog = giz.Auth(new Blog(this.blogUrl)), // autheticated blog model
                self = this;

            self.el.html('
'+_('Loading collaborators...')+'
');

            self.colabsList = [];

            blog.on('read', function()
            {
                var collaborators = this.get('Collaborator');
                collaborators.on('read', function()
                {
                    self.colabsList = this;
                    self.setupHeader.call(self, this);
                    self.setupColabStream.call(self, this);
                });
                collaborators.xfilter('Person.Id', 'Person.FullName', 'Person.EMail', 'Post').sync();
            });
            blog.sync();
        },

        update: function()
        {
            this.colabsList.each(function()
            {
                // get post list and sync it with the server
                this.get('Post').xfilter('*').sync({data: {'startEx.cId': this._latestPost}});
            });
        },

        /*!
         * display list header
         */
        setupHeader: function(colabs)
        {
            $(this.el).tmpl('livedesk>providers/colabs', {Colabs: colabs.feed('json', true)});
        },

        readPostsHandle: function()
        {

        },

        /*!
         * setup post list
         */
        setupColabStream: function(colabs)
        {
            var self = this,
                initial = colabs.count(); // used for breaking init. action
            // collaboratos list
            colabs.each(function()
            {
                var colab = this;

                colab._latestPost = 0;
                colab._viewModels = [];
                colab.on('read', function()
                {
                    // get posts for each collaborator
                    // TODO isolate the callback
                    colab.get('Post').xfilter('*')
                        .on('read', function()
                        {
                            var appendPosts = [];
                            this.each(function()
                            {

                                if( $.inArray( this.get('Id'), colab._viewModels ) === -1 )
                                {
                                    appendPosts.push(this);
                                    colab._viewModels.push(this.get('Id'));
                                }
                                //console.log(this, Math.max(colab._latestPost, parseInt(this.get('CId'))));
                                colab._latestPost = Math.max(colab._latestPost, parseInt(this.get('CId')));
                            });

                            updateItemCount += appendPosts.length;

                            appendPosts.length && $('.new-results', self.el).trigger('update.livedesk', [updateItemCount, function()
                            {
                                $(appendPosts).each(function()
                                {
                                    $('.search-result-list', self.el).prepend( (new PostView({ model: this })).render().el );
                                });
                                updateItemCount -= appendPosts.length;
                            }, initial ? true : false]);

                            initial -= 1; // decrement initial until 0 so we know when init is over and do not send

                        }).sync();

                    clearInterval(updateInterval);
                    updateInterval = setInterval(function()
                    {
                        if(!$('.search-result-list:visible', self.el).length)
                        {
                            clearInterval(updateInterval);
                            return;
                        }
                        self.update();
                    }, config.updateInterval*1000);

                }).sync();

            });
        },
        render: function()
        {

        }
    });

    $.extend( providers.colabs, { init: function(blogUrl){ new ColabView({ el: this.el, blogUrl: blogUrl }); } });

    return providers;
});<commit_msg>NR-224: Core implementation of MVC framework - part 2<commit_after>define( 'providers/colabs',
[ 'providers', 'jquery', 'gizmo/superdesk',

  config.guiJs('livedesk', 'models/blog'),
  config.guiJs('livedesk', 'models/collaborator'),

  'providers/colabs/adaptor',
  'tmpl!livedesk>providers/colabs',
  'tmpl!livedesk>providers/colabs/items' ],

function(providers, $, giz, Blog, Collaborator)
{
    var config = { updateInterval: 10 },
        colabsList = [],
        updateInterval = 0,
        intervalRunning = false,
        updateItemCount = 0;

    // single post item view
    var PostView = giz.View.extend
    ({
        init: function(opts)
        {
            this.model.on('update', this.render, this);
			this.model.on('read', this.render, this);
        },
        render: function()
        {
            var self = this;
            $.tmpl( 'livedesk>providers/colabs/items', {Posts: this.model.feed('json')}, function(e, o)
            {
                self.setElement(o);
                // make draggable
                self.el.hasClass('draggable') && self.el.draggable
                ({
                    helper: 'clone',
                    appendTo: 'body',
                    zIndex: 2700,
                    start: function(){ $(this).data('post', providers.colabs.adaptor.universal(this)); }
                });
            });
            return self;
        }
    }),
    // main view
    ColabView = giz.View.extend
    ({
        namespace: 'livedesk',
        events:
        {
            //'.collaborators-header .feed-info .label': {'click': 'toggleHeader'},
            '.new-results': {'update': 'showNewResults'}
        },

        toggleHeader: function()
        {
            var colabId = $(this).attr('data-colab-id'),
                posts = $(this).parents('.collaborators-header')
                            .nextAll('.search-result-list').find('li[data-colab-id='+colabId+']');

            if($(this).data('is-off'))
            {
                posts.show();
                $(this).addClass('label-info').data('is-off', false);
            }
            else
            {
                posts.hide();
                $(this).removeClass('label-info').data('is-off', true);
            }
        },
        /*!
         * @param e event
         * @param count
         * @param callback
         * @param wether to do update auto. or add a click-to-show button
         */
        showNewResults: function(e, count, callback, auto)
        {
            var self = $('.new-results', this.el),
                cb = function()
                {
                    self.addClass('hide');
                    callback.apply(this);
                };
            auto ? cb() : self.removeClass('hide').text( count+" "+_('new items! Update') ).one('click.livedesk', cb);
        },

        init: function()
        {
            $('.search-result-list', this.el).html('');

            $(this.el).on('click', '.collaborators-header .feed-info .label', this.toggleHeader);

            var blog = giz.Auth(new Blog(this.blogUrl)), // autheticated blog model
                self = this;

            self.el.html('
'+_('Loading collaborators...')+'
');

            self.colabsList = [];

            blog.on('read', function()
            {
                var collaborators = this.get('Collaborator');
                collaborators.on('read', function()
                {
                    self.colabsList = this;
                    self.setupHeader.call(self, this);
                    self.setupColabStream.call(self, this);
                });
                collaborators.xfilter('Person.Id', 'Person.FullName', 'Person.EMail', 'Post').sync();
            });
            blog.sync();
        },

        update: function()
        {
            this.colabsList.each(function()
            {
                // get post list and sync it with the server
                this.get('Post').xfilter('*').sync({data: {'startEx.cId': this._latestPost}});
            });
        },

        /*!
         * display list header
         */
        setupHeader: function(colabs)
        {
            $(this.el).tmpl('livedesk>providers/colabs', {Colabs: colabs.feed('json', true)});
        },
        /*!
         * initial - initial count of collaborators
         */
        readPostsHandle: function(initial, colab)
        {
            // list of new posts to append
            var appendPosts = [],
                self = this;
            this.each(function()
            {
                if( $.inArray( this.get('Id'), colab._viewModels ) === -1 )
                {
                    appendPosts.push(this);
                    colab._viewModels.push(this.get('Id'));
                }
                colab._latestPost = Math.max(colab._latestPost, parseInt(this.get('CId')));
            });
            updateItemCount += appendPosts.length;

            appendPosts.length && $('.new-results', self.el).trigger('update.livedesk', [updateItemCount, function()
            {
                $(appendPosts).each(function()
                {
                    $('.search-result-list', self.el).prepend( (new PostView({ model: this })).render().el );
                });
                updateItemCount -= appendPosts.length;
            }, initial ? true : false]);

            initial -= 1; // decrement initial until 0 so we know when init is over and do not send
        },
        /*!
         * setup post list
         */
        setupColabStream: function(colabs)
        {
            var self = this,
                initial = colabs.count(); // used for breaking init. action, decrementing until 0
            // collaboratos list
            colabs.each(function()
            {
                var colab = this;
                colab._latestPost = 0;
                colab._viewModels = [];
                colab.on('read', function()
                {
                    // get posts for each collaborator
                    colab.get('Post').xfilter('*')
                        .on('read', function(){ self.readPostsHandle.call(this, initial, colab); })
                        .sync();

                    self.startAutoUpdate();
//                    clearInterval(updateInterval);
//                    updateInterval = setInterval(function()
//                    {
//                        if(!$('.search-result-list:visible', self.el).length)
//                        {
//                            clearInterval(updateInterval);
//                            return;
//                        }
//                        self.update();
//                    }, config.updateInterval*1000);

                }).sync();

            });
        },
        startAutoUpdate: function()
        {
            var self = this;
            clearInterval(updateInterval);
            updateInterval = setInterval(function()
            {
                if(!$('.search-result-list:visible', self.el).length)
                {
                    clearInterval(updateInterval);
                    return;
                }
                self.update();
            }, config.updateInterval*1000);

            return self;
        },
        render: function()
        {

        }
    });

    var colabView = null;
    $.extend( providers.colabs, { init: function(blogUrl)
    {
        colabView = new ColabView({ el: this.el, blogUrl: blogUrl });
        this.init = function(){ return colabView.startAutoUpdate(); }
    }});

    return providers;
});<|endoftext|>


<commit_before>"use strict";
var assert = require('assert');

var Plane = function() {
  return this;
};

var Ant = function() {
  return this;
};

describe('infinite grid', function() {
  describe('grid', function() {
    it('exists', function() {
      assert(new Plane());
    });
  });

  describe('ant', function() {
    it('exists', function() {
      assert(new Ant());
    });
  });
});
<commit_msg>ants need position<commit_after>"use strict";
var assert = require('assert');

var Plane = function() {
  return this;
};

var Ant = function() {
  return this;
};

describe('infinite grid', function() {
  describe('grid', function() {
    it('exists', function() {
      assert(new Plane());
    });
  });

  describe('ant', function() {
    it('exists', function() {
      assert(new Ant());
    });

    it('has a position', function() {
      assert(new Ant().position());
    });
  });

});
<|endoftext|>