<commit_before>// Copyright 2007 The Closure Library Authors. All Rights Reserved.
//
// Licensed 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.

/**
 * @fileoverview Provides the namesspace for client-side communication
 * between pages originating from different domains (it works also
 * with pages from the same domain, but doing that is kinda
 * pointless).
 *
 * The only publicly visible class is goog.net.xpc.CrossPageChannel.
 *
 * Note: The preferred name for the main class would have been
 * CrossDomainChannel.  But as there already is a class named like
 * that (which serves a different purpose) in the maps codebase,
 * CrossPageChannel was chosen to avoid confusion.
 *
 * CrossPageChannel abstracts the underlying transport mechanism to
 * provide a common interface in all browsers.
 *
 */

/*
TODO(user)
- resolve fastback issues in Safari (IframeRelayTransport)
 */

/**
 * Namespace for CrossPageChannel
 */
goog.provide('goog.net.xpc');

goog.require('goog.debug.Logger');

/**
 * Enum used to identify transport types.
 * @enum {number}
 */
goog.net.xpc.TransportTypes = {
  NATIVE_MESSAGING: 1,
  FRAME_ELEMENT_METHOD: 2,
  IFRAME_RELAY: 3,
  IFRAME_POLLING: 4,
  FLASH: 5,
  NIX: 6
};

/**
 * Enum containing transport names. These need to correspond to the
 * transport class names for createTransport_() to work.
 * @type {Object}
 */
goog.net.xpc.TransportNames = {
  '1': 'NativeMessagingTransport',
  '2': 'FrameElementMethodTransport',
  '3': 'IframeRelayTransport',
  '4': 'IframePollingTransport',
  '5': 'FlashTransport',
  '6': 'NixTransport'
};

// TODO(user): Add auth token support to other methods.

/**
 * Field names used on configuration object.
 * @type {Object}
 */
goog.net.xpc.CfgFields = {
  /**
   * Channel name identifier.
   * Both peers have to be initialized with
   * the same channel name.  If not present, a channel name is
   * generated (which then has to transferred to the peer somehow).
   */
  CHANNEL_NAME: 'cn',
  /**
   * Authorization token. If set, NIX will use this authorization token
   * to validate the setup.
   */
  AUTH_TOKEN: 'at',
  /**
   * Remote party's authorization token. If set, NIX will validate this
   * authorization token against that sent by the other party.
   */
  REMOTE_AUTH_TOKEN: 'rat',
  /**
   * The URI of the peer page.
   */
  PEER_URI: 'pu',
  /**
   * Ifame-ID identifier.
   * The id of the iframe element the peer-document lives in.
   */
  IFRAME_ID: 'ifrid',
  /**
   * Transport type identifier.
   * The transport type to use. Possible values are entries from
   * goog.net.xpc.TransportTypes. If not present, the transport is
   * determined automatically based on the useragent's capabilities.
   */
  TRANSPORT: 'tp',
  /**
   * Local relay URI identifier (IframeRelayTransport-specific).
   * The URI (can't contain a fragment identifier) used by the peer to
   * relay data through.
   */
  LOCAL_RELAY_URI: 'lru',
  /**
   * Peer relay URI identifier (IframeRelayTransport-specific).
   * The URI (can't contain a fragment identifier) used to relay data
   * to the peer.
   */
  PEER_RELAY_URI: 'pru',
  /**
   * Local poll URI identifier (IframePollingTransport-specific).
   * The URI  (can't contain a fragment identifier)which is polled
   * to receive data from the peer.
   */
  LOCAL_POLL_URI: 'lpu',
  /**
   * Local poll URI identifier (IframePollingTransport-specific).
   * The URI (can't contain a fragment identifier) used to send data
   * to the peer.
   */
  PEER_POLL_URI: 'ppu',
  /**
   * The hostname of the peer window, including protocol, domain, and port
   * (if specified). Used for security sensitive applications that make
   * use of NativeMessagingTransport (i.e. most applications).
   */
  PEER_HOSTNAME: 'ph'
};

/**
 * @enum {number}
 */
goog.net.xpc.ChannelStates = {
  NOT_CONNECTED: 1,
  CONNECTED: 2,
  CLOSED: 3
};

/**
 * The name of the transport service (used for internal signalling).
 * @type {string}
 * @private
 */
goog.net.xpc.TRANSPORT_SERVICE_ = 'tp';

/**
 * Transport signaling message: setup.
 * @protected
 */
goog.net.xpc.SETUP = 'SETUP';

/**
 * Transport signaling message: setup acknowledgement.
 * @private
 */
goog.net.xpc.SETUP_ACK_ = 'SETUP_ACK';

/**
 * Object holding active channels.
 * @type {Object}
 * @private
 */
goog.net.xpc.channels_ = {};

/**
 * Returns a random string.
 * @param {number} length How many characters the string shall contain.
 * @param {string=} opt_characters The characters used.
 * @return {string} The random string.
 */
goog.net.xpc.getRandomString = function(length, opt_characters) {
  var chars = opt_characters || goog.net.xpc.randomStringCharacters_;
  var charsLength = chars.length;
  var s = '';
  while (length-- > 0) {
    s += chars.charAt(Math.floor(Math.random() * charsLength));
  }
  return s;
};

/**
 * The default characters used for random string generation.
 * @type {string}
 * @private
 */
goog.net.xpc.randomStringCharacters_ =
    'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';

/**
 * The logger.
 * @type {goog.debug.Logger}
 */
goog.net.xpc.logger = goog.debug.Logger.getLogger('goog.net.xpc');
<commit_msg><commit_after>// Copyright 2007 The Closure Library Authors. All Rights Reserved.
//
// Licensed 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.

/**
 * @fileoverview Provides the namesspace for client-side communication
 * between pages originating from different domains (it works also
 * with pages from the same domain, but doing that is kinda
 * pointless).
 *
 * The only publicly visible class is goog.net.xpc.CrossPageChannel.
 *
 * Note: The preferred name for the main class would have been
 * CrossDomainChannel.  But as there already is a class named like
 * that (which serves a different purpose) in the maps codebase,
 * CrossPageChannel was chosen to avoid confusion.
 *
 * CrossPageChannel abstracts the underlying transport mechanism to
 * provide a common interface in all browsers.
 *
 */

/*
TODO(user)
- resolve fastback issues in Safari (IframeRelayTransport)
 */

/**
 * Namespace for CrossPageChannel
 */
goog.provide('goog.net.xpc');
goog.provide('goog.net.xpc.CfgFields');
goog.provide('goog.net.xpc.ChannelStates');
goog.provide('goog.net.xpc.TransportNames');
goog.provide('goog.net.xpc.TransportTypes');
goog.provide('goog.net.xpc.UriCfgFields');

goog.require('goog.debug.Logger');

/**
 * Enum used to identify transport types.
 * @enum {number}
 */
goog.net.xpc.TransportTypes = {
  NATIVE_MESSAGING: 1,
  FRAME_ELEMENT_METHOD: 2,
  IFRAME_RELAY: 3,
  IFRAME_POLLING: 4,
  FLASH: 5,
  NIX: 6
};

/**
 * Enum containing transport names. These need to correspond to the
 * transport class names for createTransport_() to work.
 * @type {Object}
 */
goog.net.xpc.TransportNames = {
  '1': 'NativeMessagingTransport',
  '2': 'FrameElementMethodTransport',
  '3': 'IframeRelayTransport',
  '4': 'IframePollingTransport',
  '5': 'FlashTransport',
  '6': 'NixTransport'
};

// TODO(user): Add auth token support to other methods.

/**
 * Field names used on configuration object.
 * @type {Object}
 */
goog.net.xpc.CfgFields = {
  /**
   * Channel name identifier.
   * Both peers have to be initialized with
   * the same channel name.  If not present, a channel name is
   * generated (which then has to transferred to the peer somehow).
   */
  CHANNEL_NAME: 'cn',
  /**
   * Authorization token. If set, NIX will use this authorization token
   * to validate the setup.
   */
  AUTH_TOKEN: 'at',
  /**
   * Remote party's authorization token. If set, NIX will validate this
   * authorization token against that sent by the other party.
   */
  REMOTE_AUTH_TOKEN: 'rat',
  /**
   * The URI of the peer page.
   */
  PEER_URI: 'pu',
  /**
   * Ifame-ID identifier.
   * The id of the iframe element the peer-document lives in.
   */
  IFRAME_ID: 'ifrid',
  /**
   * Transport type identifier.
   * The transport type to use. Possible values are entries from
   * goog.net.xpc.TransportTypes. If not present, the transport is
   * determined automatically based on the useragent's capabilities.
   */
  TRANSPORT: 'tp',
  /**
   * Local relay URI identifier (IframeRelayTransport-specific).
   * The URI (can't contain a fragment identifier) used by the peer to
   * relay data through.
   */
  LOCAL_RELAY_URI: 'lru',
  /**
   * Peer relay URI identifier (IframeRelayTransport-specific).
   * The URI (can't contain a fragment identifier) used to relay data
   * to the peer.
   */
  PEER_RELAY_URI: 'pru',
  /**
   * Local poll URI identifier (IframePollingTransport-specific).
   * The URI  (can't contain a fragment identifier)which is polled
   * to receive data from the peer.
   */
  LOCAL_POLL_URI: 'lpu',
  /**
   * Local poll URI identifier (IframePollingTransport-specific).
   * The URI (can't contain a fragment identifier) used to send data
   * to the peer.
   */
  PEER_POLL_URI: 'ppu',
  /**
   * The hostname of the peer window, including protocol, domain, and port
   * (if specified). Used for security sensitive applications that make
   * use of NativeMessagingTransport (i.e. most applications).
   */
  PEER_HOSTNAME: 'ph'
};

/**
 * @enum {number}
 */
goog.net.xpc.ChannelStates = {
  NOT_CONNECTED: 1,
  CONNECTED: 2,
  CLOSED: 3
};

/**
 * The name of the transport service (used for internal signalling).
 * @type {string}
 * @private
 */
goog.net.xpc.TRANSPORT_SERVICE_ = 'tp';

/**
 * Transport signaling message: setup.
 * @protected
 */
goog.net.xpc.SETUP = 'SETUP';

/**
 * Transport signaling message: setup acknowledgement.
 * @private
 */
goog.net.xpc.SETUP_ACK_ = 'SETUP_ACK';

/**
 * Object holding active channels.
 * @type {Object}
 * @private
 */
goog.net.xpc.channels_ = {};

/**
 * Returns a random string.
 * @param {number} length How many characters the string shall contain.
 * @param {string=} opt_characters The characters used.
 * @return {string} The random string.
 */
goog.net.xpc.getRandomString = function(length, opt_characters) {
  var chars = opt_characters || goog.net.xpc.randomStringCharacters_;
  var charsLength = chars.length;
  var s = '';
  while (length-- > 0) {
    s += chars.charAt(Math.floor(Math.random() * charsLength));
  }
  return s;
};

/**
 * The default characters used for random string generation.
 * @type {string}
 * @private
 */
goog.net.xpc.randomStringCharacters_ =
    'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';

/**
 * The logger.
 * @type {goog.debug.Logger}
 */
goog.net.xpc.logger = goog.debug.Logger.getLogger('goog.net.xpc');
<|endoftext|>


<commit_before>// Copyright 2008 The Closure Library Authors. All Rights Reserved.
//
// Licensed 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.

/**
 * @fileoverview Global renderer and decorator registry.
 */

goog.provide('goog.ui.registry');

goog.require('goog.dom.classes');

/**
 * Given a {@link goog.ui.Component} constructor, returns an instance of its
 * default renderer.  If the default renderer is a singleton, returns the
 * singleton instance; otherwise returns a new instance of the renderer class.
 * @param {Function} componentCtor Component constructor function (for example
 *     {@code goog.ui.Button}).
 * @return {goog.ui.ControlRenderer?} Renderer instance (for example the
 *     singleton instance of {@code goog.ui.ButtonRenderer}), or null if
 *     no default renderer was found.
 */
goog.ui.registry.getDefaultRenderer = function(componentCtor) {
  // Locate the default renderer based on the constructor's unique ID.  If no
  // renderer is registered for this class, walk up the superClass_ chain.
  var key;
  var /** @constructor */ rendererCtor;
  while (componentCtor) {
    key = goog.getUid(componentCtor);
    if ((rendererCtor = goog.ui.registry.defaultRenderers_[key])) {
      break;
    }
    componentCtor = componentCtor.superClass_ ?
        componentCtor.superClass_.constructor : null;
  }

  // If the renderer has a static getInstance method, return the singleton
  // instance; otherwise create and return a new instance.
  if (rendererCtor) {
    return goog.isFunction(rendererCtor.getInstance) ?
        rendererCtor.getInstance() : new rendererCtor();
  }

  return null;
};

/**
 * Sets the default renderer for the given {@link goog.ui.Component}
 * constructor.
 * @param {Function} componentCtor Component constructor function (for example
 *     {@code goog.ui.Button}).
 * @param {Function} rendererCtor Renderer constructor function (for example
 *     {@code goog.ui.ButtonRenderer}).
 * @throws {Error} If the arguments aren't functions.
 */
goog.ui.registry.setDefaultRenderer = function(componentCtor, rendererCtor) {
  // In this case, explicit validation has negligible overhead (since each
  // renderer is only registered once), and helps catch subtle bugs.
  if (!goog.isFunction(componentCtor)) {
    throw Error('Invalid component class ' + componentCtor);
  }
  if (!goog.isFunction(rendererCtor)) {
    throw Error('Invalid renderer class ' + rendererCtor);
  }

  // Map the component constructor's unique ID to the renderer constructor.
  var key = goog.getUid(componentCtor);
  goog.ui.registry.defaultRenderers_[key] = rendererCtor;
};

/**
 * Returns the {@link goog.ui.Component} instance created by the decorator
 * factory function registered for the given CSS class name, or null if no
 * decorator factory function was found.
 * @param {string} className CSS class name.
 * @return {goog.ui.Component?} Component instance.
 */
goog.ui.registry.getDecoratorByClassName = function(className) {
  return className in goog.ui.registry.decoratorFunctions_ ?
      goog.ui.registry.decoratorFunctions_[className]() : null;
};

/**
 * Maps a CSS class name to a function that returns a new instance of
 * {@link goog.ui.Component} or a subclass, suitable to decorate an element
 * that has the specified CSS class.
 * @param {string} className CSS class name.
 * @param {Function} decoratorFn No-argument function that returns a new
 *     instance of a {@link goog.ui.Component} to decorate an element.
 * @throws {Error} If the class name or the decorator function is invalid.
 */
goog.ui.registry.setDecoratorByClassName = function(className, decoratorFn) {
  // In this case, explicit validation has negligible overhead (since each
  // decorator  is only registered once), and helps catch subtle bugs.
  if (!className) {
    throw Error('Invalid class name ' + className);
  }
  if (!goog.isFunction(decoratorFn)) {
    throw Error('Invalid decorator function ' + decoratorFn);
  }

  goog.ui.registry.decoratorFunctions_[className] = decoratorFn;
};

/**
 * Returns an instance of {@link goog.ui.Component} or a subclass suitable to
 * decorate the given element, based on its CSS class.
 * @param {Element} element Element to decorate.
 * @return {goog.ui.Component?} Component to decorate the element (null if
 *     none).
 */
goog.ui.registry.getDecorator = function(element) {
  var decorator;
  var classNames = goog.dom.classes.get(element);
  for (var i = 0, len = classNames.length; i < len; i++) {
    if ((decorator = goog.ui.registry.getDecoratorByClassName(classNames[i]))) {
      return decorator;
    }
  }
  return null;
};

/**
 * Resets the global renderer and decorator registry.
 */
goog.ui.registry.reset = function() {
  goog.ui.registry.defaultRenderers_ = {};
  goog.ui.registry.decoratorFunctions_ = {};
};

/**
 * Map of {@link goog.ui.Component} constructor unique IDs to the constructors
 * of their default {@link goog.ui.Renderer}s.
 * @type {Object}
 * @private
 */
goog.ui.registry.defaultRenderers_ = {};

/**
 * Map of CSS class names to registry factory functions.  The keys are
 * class names.  The values are function objects that return new instances
 * of {@link goog.ui.registry} or one of its subclasses, suitable to
 * decorate elements marked with the corresponding CSS class.  Used by
 * containers while decorating their children.
 * @type {Object}
 * @private
 */
goog.ui.registry.decoratorFunctions_ = {};
<commit_msg><commit_after>// Copyright 2008 The Closure Library Authors. All Rights Reserved.
//
// Licensed 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.

/**
 * @fileoverview Global renderer and decorator registry.
 */

goog.provide('goog.ui.registry');

goog.require('goog.dom.classes');

/**
 * Given a {@link goog.ui.Component} constructor, returns an instance of its
 * default renderer.  If the default renderer is a singleton, returns the
 * singleton instance; otherwise returns a new instance of the renderer class.
 * @param {Function} componentCtor Component constructor function (for example
 *     {@code goog.ui.Button}).
 * @return {goog.ui.ControlRenderer?} Renderer instance (for example the
 *     singleton instance of {@code goog.ui.ButtonRenderer}), or null if
 *     no default renderer was found.
 */
goog.ui.registry.getDefaultRenderer = function(componentCtor) {
  // Locate the default renderer based on the constructor's unique ID.  If no
  // renderer is registered for this class, walk up the superClass_ chain.
  var key;
  /** @type {Function|undefined} */ var rendererCtor;
  while (componentCtor) {
    key = goog.getUid(componentCtor);
    if ((rendererCtor = goog.ui.registry.defaultRenderers_[key])) {
      break;
    }
    componentCtor = componentCtor.superClass_ ?
        componentCtor.superClass_.constructor : null;
  }

  // If the renderer has a static getInstance method, return the singleton
  // instance; otherwise create and return a new instance.
  if (rendererCtor) {
    return goog.isFunction(rendererCtor.getInstance) ?
        rendererCtor.getInstance() : new rendererCtor();
  }

  return null;
};

/**
 * Sets the default renderer for the given {@link goog.ui.Component}
 * constructor.
 * @param {Function} componentCtor Component constructor function (for example
 *     {@code goog.ui.Button}).
 * @param {Function} rendererCtor Renderer constructor function (for example
 *     {@code goog.ui.ButtonRenderer}).
 * @throws {Error} If the arguments aren't functions.
 */
goog.ui.registry.setDefaultRenderer = function(componentCtor, rendererCtor) {
  // In this case, explicit validation has negligible overhead (since each
  // renderer is only registered once), and helps catch subtle bugs.
  if (!goog.isFunction(componentCtor)) {
    throw Error('Invalid component class ' + componentCtor);
  }
  if (!goog.isFunction(rendererCtor)) {
    throw Error('Invalid renderer class ' + rendererCtor);
  }

  // Map the component constructor's unique ID to the renderer constructor.
  var key = goog.getUid(componentCtor);
  goog.ui.registry.defaultRenderers_[key] = rendererCtor;
};

/**
 * Returns the {@link goog.ui.Component} instance created by the decorator
 * factory function registered for the given CSS class name, or null if no
 * decorator factory function was found.
 * @param {string} className CSS class name.
 * @return {goog.ui.Component?} Component instance.
 */
goog.ui.registry.getDecoratorByClassName = function(className) {
  return className in goog.ui.registry.decoratorFunctions_ ?
      goog.ui.registry.decoratorFunctions_[className]() : null;
};

/**
 * Maps a CSS class name to a function that returns a new instance of
 * {@link goog.ui.Component} or a subclass, suitable to decorate an element
 * that has the specified CSS class.
 * @param {string} className CSS class name.
 * @param {Function} decoratorFn No-argument function that returns a new
 *     instance of a {@link goog.ui.Component} to decorate an element.
 * @throws {Error} If the class name or the decorator function is invalid.
 */
goog.ui.registry.setDecoratorByClassName = function(className, decoratorFn) {
  // In this case, explicit validation has negligible overhead (since each
  // decorator  is only registered once), and helps catch subtle bugs.
  if (!className) {
    throw Error('Invalid class name ' + className);
  }
  if (!goog.isFunction(decoratorFn)) {
    throw Error('Invalid decorator function ' + decoratorFn);
  }

  goog.ui.registry.decoratorFunctions_[className] = decoratorFn;
};

/**
 * Returns an instance of {@link goog.ui.Component} or a subclass suitable to
 * decorate the given element, based on its CSS class.
 * @param {Element} element Element to decorate.
 * @return {goog.ui.Component?} Component to decorate the element (null if
 *     none).
 */
goog.ui.registry.getDecorator = function(element) {
  var decorator;
  var classNames = goog.dom.classes.get(element);
  for (var i = 0, len = classNames.length; i < len; i++) {
    if ((decorator = goog.ui.registry.getDecoratorByClassName(classNames[i]))) {
      return decorator;
    }
  }
  return null;
};

/**
 * Resets the global renderer and decorator registry.
 */
goog.ui.registry.reset = function() {
  goog.ui.registry.defaultRenderers_ = {};
  goog.ui.registry.decoratorFunctions_ = {};
};

/**
 * Map of {@link goog.ui.Component} constructor unique IDs to the constructors
 * of their default {@link goog.ui.Renderer}s.
 * @type {Object}
 * @private
 */
goog.ui.registry.defaultRenderers_ = {};

/**
 * Map of CSS class names to registry factory functions.  The keys are
 * class names.  The values are function objects that return new instances
 * of {@link goog.ui.registry} or one of its subclasses, suitable to
 * decorate elements marked with the corresponding CSS class.  Used by
 * containers while decorating their children.
 * @type {Object}
 * @private
 */
goog.ui.registry.decoratorFunctions_ = {};
<|endoftext|>


<commit_before>cronenberg.DashboardHolder = function(url_, element_) {
    this.url = url_;
    this.dashboard = null;
    this.element = element_;

    this.setRange = function(from, until) {
        var url = URI(this.url);
        if (from) {
            url.setQuery('from', from);
        }
        if (until) {
            url.setQuery('until', until);
        }
        this.url = url.href();
    };
};

cronenberg.DashboardManager = function() {
    this.current = null,

    /**
     * Register an event handler for processing a dashboard once it's
     * loaded and ready.
     */
    this.onDashboardLoaded = function(handler) {
        bean.on(cronenberg.dashboards, cronenberg.events.DASHBOARD_LOADED, handler);
        return this;
    };

    this.load = function(url, element) {
        cronenberg.current = new cronenberg.DashboardHolder(url, element);
        var current = cronenberg.current;
        $.ajax({
            dataType: "json",
            url: url
        }).done(function(data) {
            var dashboard = data.entities[0];
            current.dashboard = dashboard;

            // Build a map from the presentation elements to their
            // model objects.
            dashboard.elementToItemMap = {};
            _.each(dashboard.grid.rows, function(row) {
                if (row.item_type == 'row') {
                    _.each(row.cells, function(cell) {
                        _.each(cell.presentation, function(presentation) {
                            if (typeof(presentation.element_id) != "undefined") {
                                dashboard.elementToItemMap[presentation.element_id] = presentation;
                            }
                        });
                    });
                }
            });

            // Set up the queries
            cronenberg.queries.clear();
            for (var query_name in dashboard.queries) {
                cronenberg.queries.add(query_name, dashboard.queries[query_name]);
            }

            // Render the dashboard
            var rendered = cronenberg.templates.render(dashboard);
            $(current.element).html(rendered);

            var currentURL = URI(current.url);

            bean.fire(this, 'ds-range-changed', currentURL.query('from'), currentURL.query('until'));

            // Load the queries
            cronenberg.queries.loadAll();

            bean.fire(cronenberg.dashboards, cronenberg.events.DASHBOARD_LOADED, dashboard);
        });
    };

    this.refresh = function() {
        var self = cronenberg.dashboards;
        if (self.current) {
            self.load(self.current.url);
        }
    };

    this.set_time_range = function(from, until) {
        var current = cronenberg.current;
        var location = URI(window.location).setQuery('from', from).href();
        window.history.pushState({url: current.url, element:current.element}, '', location);

        current.setRange(from, until);
        bean.fire(this, 'ds-range-changed', from, until);
        cronenberg.dashboards.load(current.url, current.element);
    };

    this.ranges = {
        // TODO - quick hack. Parse the range and generate on the fly
        // for maximum flexibiliy
        '-1h' : 'Past Hour',
        '-2h' : 'Past Two Hours',
        '-3h' : 'Past Three Hours',
        '-4h' : 'Past Four Hours',
        '-6h' : 'Past Six Hours',
        '-1d' : 'Past Day',
        '-7d' : 'Past Week'
    };

    this.onRangeChanged = function(handler) {
        bean.on(this, 'ds-range-changed', handler);
    };

    this.autoRefreshInterval = null;
    this.intervalId = null;

    this.set_refresh_interval = function(value) {
        var self = this;
        var intervalSeconds = parseInt(value);
        self.autoRefreshInterval = intervalSeconds;
        if (self.intervalId) {
            window.clearInterval(self.intervalId);
        }
        if (intervalSeconds > 0) {
            self.intervalSeconds = intervalSeconds;
            self.intervalId = window.setInterval(self.refresh, intervalSeconds * 1000);
        }
    };
};

cronenberg.dashboards = new cronenberg.DashboardManager();
<commit_msg>Safer scoping with self<commit_after>cronenberg.DashboardHolder = function(url_, element_) {
    this.url = url_;
    this.dashboard = null;
    this.element = element_;

    this.setRange = function(from, until) {
        var url = URI(this.url);
        if (from) {
            url.setQuery('from', from);
        }
        if (until) {
            url.setQuery('until', until);
        }
        this.url = url.href();
    };
};

cronenberg.DashboardManager = function() {
    this.current = null,

    /**
     * Register an event handler for processing a dashboard once it's
     * loaded and ready.
     */
    this.onDashboardLoaded = function(handler) {
        bean.on(cronenberg.dashboards, cronenberg.events.DASHBOARD_LOADED, handler);
        return this;
    };

    this.load = function(url, element) {
        var self = this;
        self.current = new cronenberg.DashboardHolder(url, element);
        $.ajax({
            dataType: "json",
            url: url
        }).done(function(data) {
            var dashboard = data.entities[0];
            self.current.dashboard = dashboard;

            // Build a map from the presentation elements to their
            // model objects.
            dashboard.elementToItemMap = {};
            _.each(dashboard.grid.rows, function(row) {
                if (row.item_type == 'row') {
                    _.each(row.cells, function(cell) {
                        _.each(cell.presentation, function(presentation) {
                            if (typeof(presentation.element_id) != "undefined") {
                                dashboard.elementToItemMap[presentation.element_id] = presentation;
                            }
                        });
                    });
                }
            });

            // Set up the queries
            cronenberg.queries.clear();
            for (var query_name in dashboard.queries) {
                cronenberg.queries.add(query_name, dashboard.queries[query_name]);
            }

            // Render the dashboard
            var rendered = cronenberg.templates.render(dashboard);
            $(self.current.element).html(rendered);

            var currentURL = URI(self.current.url);

            bean.fire(this, 'ds-range-changed', currentURL.query('from'), currentURL.query('until'));

            // Load the queries
            cronenberg.queries.loadAll();

            bean.fire(cronenberg.dashboards, cronenberg.events.DASHBOARD_LOADED, dashboard);
        });
    };

    this.refresh = function() {
        var self = this;
        if (self.current) {
            self.load(self.current.url);
        }
    };

    this.set_time_range = function(from, until) {
        var self = this;
        var location = URI(window.location).setQuery('from', from).href();
        window.history.pushState({url: self.current.url, element:self.current.element}, '', location);

        self.current.setRange(from, until);
        bean.fire(this, 'ds-range-changed', from, until);
        cronenberg.dashboards.load(self.current.url, self.current.element);
    };

    this.ranges = {
        // TODO - quick hack. Parse the range and generate on the fly
        // for maximum flexibiliy
        '-1h' : 'Past Hour',
        '-2h' : 'Past Two Hours',
        '-3h' : 'Past Three Hours',
        '-4h' : 'Past Four Hours',
        '-6h' : 'Past Six Hours',
        '-1d' : 'Past Day',
        '-7d' : 'Past Week'
    };

    this.onRangeChanged = function(handler) {
        var self = this;
        bean.on(self, 'ds-range-changed', handler);
    };

    this.autoRefreshInterval = null;
    this.intervalId = null;

    this.set_refresh_interval = function(value) {
        var self = this;
        var intervalSeconds = parseInt(value);
        self.autoRefreshInterval = intervalSeconds;
        if (self.intervalId) {
            window.clearInterval(self.intervalId);
        }
        if (intervalSeconds > 0) {
            self.intervalSeconds = intervalSeconds;
            self.intervalId = window.setInterval(self.refresh, intervalSeconds * 1000);
        }
    };
};

cronenberg.dashboards = new cronenberg.DashboardManager();
<|endoftext|>


<commit_before>context('MultiSelectDialog', () => {
	before(() => {
		cy.login();
		cy.visit('/app');
		const contact_template = {
			"doctype": "Contact",
			"first_name": "Test",
			"status": "Passive",
			"email_ids": [
				{
					"doctype": "Contact Email",
					"email_id": "test@example.com",
					"is_primary": 0
				}
			]
		};
		const promises = Array.from({length: 25})
			.map(() => cy.insert_doc('Contact', contact_template, true));
		Promise.all(promises);
	});

	function open_multi_select_dialog() {
		cy.window().its('frappe').then(frappe => {
			new frappe.ui.form.MultiSelectDialog({
				doctype: "Contact",
				target: {},
				setters: {
					status: null,
					gender: null
				},
				add_filters_group: 1,
				allow_child_item_selection: 1,
				child_fieldname: "email_ids",
				child_columns: ["email_id", "is_primary"]
			});
		});
	}

	it('checks multi select dialog api works', () => {
		open_multi_select_dialog();
		cy.get_open_dialog().should('contain', 'Select Contacts');
	});

	it('checks for filters', () => {
		['search_term', 'status', 'gender'].forEach(fieldname => {
			cy.get_open_dialog().get(`.frappe-control[data-fieldname="${fieldname}"]`).should('exist');
		});

		// add_filters_group: 1 should add a filter group
		cy.get_open_dialog().get(`.frappe-control[data-fieldname="filter_area"]`).should('exist');

	});

	it('checks for child item selection', () => {
		cy.get_open_dialog()
			.get(`.dt-row-header`).should('not.exist');

		cy.get_open_dialog()
			.get(`.frappe-control[data-fieldname="allow_child_item_selection"]`)
			.find('input[data-fieldname="allow_child_item_selection"]')
			.should('exist')
			.click({force: true});

		cy.get_open_dialog()
			.get(`.frappe-control[data-fieldname="child_selection_area"]`)
			.should('exist');

		cy.get_open_dialog()
			.get(`.dt-row-header`).should('contain', 'Contact');

		cy.get_open_dialog()
			.get(`.dt-row-header`).should('contain', 'Email Id');

		cy.get_open_dialog()
			.get(`.dt-row-header`).should('contain', 'Is Primary');
	});

	it('tests more button', () => {
		cy.get_open_dialog()
			.get(`.frappe-control[data-fieldname="more_btn"]`)
			.should('exist')
			.as('more-btn');

		cy.get_open_dialog().get('.list-item-container').should(($rows) => {
			expect($rows).to.have.length(20);
		});

		cy.intercept('POST', 'api/method/frappe.client.get_list').as('get-more-records');
		cy.get('@more-btn').find('button').click({force: true});
		cy.wait('@get-more-records');

		cy.get_open_dialog().get('.list-item-container').should(($rows) => {
			if ($rows.length <= 20) {
				throw new Error("More button doesn't work");
			}
		});

	});
});<commit_msg>test: fixed failing multi select dialog test<commit_after>context('MultiSelectDialog', () => {
	before(() => {
		cy.login();
		cy.visit('/app');
		const contact_template = {
			"doctype": "Contact",
			"first_name": "Test",
			"status": "Passive",
			"email_ids": [
				{
					"doctype": "Contact Email",
					"email_id": "test@example.com",
					"is_primary": 0
				}
			]
		};
		const promises = Array.from({length: 25})
			.map(() => cy.insert_doc('Contact', contact_template, true));
		Promise.all(promises);
	});

	function open_multi_select_dialog() {
		cy.window().its('frappe').then(frappe => {
			new frappe.ui.form.MultiSelectDialog({
				doctype: "Contact",
				target: {},
				setters: {
					status: null,
					gender: null
				},
				add_filters_group: 1,
				allow_child_item_selection: 1,
				child_fieldname: "email_ids",
				child_columns: ["email_id", "is_primary"]
			});
		});
	}

	it('checks multi select dialog api works', () => {
		open_multi_select_dialog();
		cy.get_open_dialog().should('contain', 'Select Contacts');
	});

	it('checks for filters', () => {
		['search_term', 'status', 'gender'].forEach(fieldname => {
			cy.get_open_dialog().get(`.frappe-control[data-fieldname="${fieldname}"]`).should('exist');
		});

		// add_filters_group: 1 should add a filter group
		cy.get_open_dialog().get(`.frappe-control[data-fieldname="filter_area"]`).should('exist');

	});

	it('checks for child item selection', () => {
		cy.get_open_dialog()
			.get(`.dt-row-header`).should('not.exist');

		cy.get_open_dialog()
			.get(`.frappe-control[data-fieldname="allow_child_item_selection"]`)
			.find('input[data-fieldname="allow_child_item_selection"]')
			.should('exist')
			.click({force: true});

		cy.get_open_dialog()
			.get(`.frappe-control[data-fieldname="child_selection_area"]`)
			.should('exist');

		cy.get_open_dialog()
			.get(`.dt-row-header`).should('contain', 'Contact');

		cy.get_open_dialog()
			.get(`.dt-row-header`).should('contain', 'Email Id');

		cy.get_open_dialog()
			.get(`.dt-row-header`).should('contain', 'Is Primary');
	});

	it('tests more button', () => {
		cy.get_open_dialog()
			.get(`.frappe-control[data-fieldname="more_child_btn"]`)
			.should('exist')
			.as('more-btn');

		cy.get_open_dialog().get('.datatable .dt-scrollable .dt-row').should(($rows) => {
			expect($rows).to.have.length(20);
		});

		cy.intercept('POST', 'api/method/frappe.client.get_list').as('get-more-records');
		cy.get('@more-btn').find('button').click({force: true});
		cy.wait('@get-more-records');

		cy.get_open_dialog().get('.datatable .dt-scrollable .dt-row').should(($rows) => {
			if ($rows.length <= 20) {
				throw new Error("More button doesn't work");
			}
		});

	});
});<|endoftext|>


<commit_before>'use strict'

const { remote, shell } = require('electron')
const { Menu, app } = remote
const EventEmitter = require('events')
const path = require('path')
const packagePath = path.join(__dirname, '..', '..', 'package.json')
const packageInfo = require(packagePath)

class AppMenu extends EventEmitter {
  constructor(options) {
    super()
    this.options = options
    this.template = []
    this.altOrOption = process.platform === 'darwin' ? 'Option' : 'Alt'
    const self = this
    this.aboutOption = {
      label: `About ${app.getName()}`,
      click() { self.emit('about-app') },
    }
    this.bugReportOption = {
      label: 'Report a bug',
      click() { shell.openExternal(packageInfo.bugs.url) },
    }
    this.authOption = {
      label: 'Authenticate',
      click() { self.emit('authenticate') },
    }
    this.buildMenu()
    this.menu = Menu.buildFromTemplate(this.template)
    Menu.setApplicationMenu(this.menu)
  }

  buildMenu() {
    if (process.platform === 'darwin') {
      this.buildOSXMenu()
    } else {
      this.buildNonOSXMenu()
    }
  }

  getEditMenu() {
    return {
      label: 'Edit',
      submenu: [
        { label: 'Undo', accelerator: 'CmdOrCtrl+Z', selector: 'undo:' },
        { label: 'Redo', accelerator: 'Shift+CmdOrCtrl+Z', selector: 'redo:' },
        { type: 'separator' },
        { label: 'Cut', accelerator: 'CmdOrCtrl+X', selector: 'cut:' },
        { label: 'Copy', accelerator: 'CmdOrCtrl+C', selector: 'copy:' },
        { label: 'Paste', accelerator: 'CmdOrCtrl+V', selector: 'paste:' },
        {
          label: 'Select All',
          accelerator: 'CmdOrCtrl+A',
          selector: 'selectAll:',
        },
      ],
    }
  }

  setIsAuthenticated(isAuthenticated) {
    this.options = this.options || {}
    this.options.isAuthenticated = isAuthenticated
    const viewMenu = this.menu.items[2].submenu
    viewMenu.items[0].enabled = isAuthenticated // Tasks
    viewMenu.items[1].enabled = isAuthenticated // Filters
  }

  getViewMenu() {
    const self = this
    return {
      label: 'View',
      submenu: [
        {
          label: 'Tasks',
          accelerator: 'CmdOrCtrl+1',
          click() { self.emit('tasks') },
          enabled: this.options.isAuthenticated,
        },
        {
          label: 'Filters',
          accelerator: 'CmdOrCtrl+2',
          click() { self.emit('filters') },
          enabled: this.options.isAuthenticated,
        },
        {
          label: 'Authentication',
          accelerator: 'CmdOrCtrl+3',
          click() { self.emit('authenticate') },
        },
      ],
    }
  }

  getToolsMenu() {
    return {
      label: 'Tools',
      submenu: [
        {
          label: 'Developer Tools',
          accelerator: `CmdOrCtrl+${this.altOrOption}+I`,
          click(item, win) {
            if (win) {
              win.webContents.toggleDevTools()
            }
          },
        },
      ],
    }
  }

  getAppMenu() {
    return {
      label: app.getName(),
      submenu: [
        this.aboutOption,
        // { label: 'About', selector: 'orderFrontStandardAboutPanel:' },
        this.authOption,
        { type: 'separator' },
        {
          label: 'Quit',
          accelerator: 'Command+Q',
          click() { app.quit() },
        },
      ],
    }
  }

  buildOSXMenu() {
    this.template.push(this.getAppMenu())
    this.template.push(this.getEditMenu())
    this.template.push(this.getViewMenu())
    this.template.push(this.getToolsMenu())
    this.template.push({
      label: 'Help',
      role: 'help',
      submenu: [
        this.bugReportOption,
      ],
    })
  }

  getFileMenu() {
    return {
      label: 'File',
      submenu: [
        this.authOption,
        {
          label: 'Exit',
          accelerator: 'Ctrl+Q',
          click() { app.quit() },
        },
      ],
    }
  }

  buildNonOSXMenu() {
    this.template.push(this.getFileMenu())
    this.template.push(this.getEditMenu())
    this.template.push(this.getViewMenu())
    this.template.push(this.getToolsMenu())
    this.template.push({
      label: 'Help',
      submenu: [
        this.aboutOption,
        this.bugReportOption,
      ],
    })
  }
}

module.exports = AppMenu
<commit_msg>Remove Authenticate from app/File menu<commit_after>'use strict'

const { remote, shell } = require('electron')
const { Menu, app } = remote
const EventEmitter = require('events')
const path = require('path')
const packagePath = path.join(__dirname, '..', '..', 'package.json')
const packageInfo = require(packagePath)

class AppMenu extends EventEmitter {
  constructor(options) {
    super()
    this.options = options
    this.template = []
    this.altOrOption = process.platform === 'darwin' ? 'Option' : 'Alt'
    const self = this
    this.aboutOption = {
      label: `About ${app.getName()}`,
      click() { self.emit('about-app') },
    }
    this.bugReportOption = {
      label: 'Report a bug',
      click() { shell.openExternal(packageInfo.bugs.url) },
    }
    this.buildMenu()
    this.menu = Menu.buildFromTemplate(this.template)
    Menu.setApplicationMenu(this.menu)
  }

  buildMenu() {
    if (process.platform === 'darwin') {
      this.buildOSXMenu()
    } else {
      this.buildNonOSXMenu()
    }
  }

  getEditMenu() {
    return {
      label: 'Edit',
      submenu: [
        { label: 'Undo', accelerator: 'CmdOrCtrl+Z', selector: 'undo:' },
        { label: 'Redo', accelerator: 'Shift+CmdOrCtrl+Z', selector: 'redo:' },
        { type: 'separator' },
        { label: 'Cut', accelerator: 'CmdOrCtrl+X', selector: 'cut:' },
        { label: 'Copy', accelerator: 'CmdOrCtrl+C', selector: 'copy:' },
        { label: 'Paste', accelerator: 'CmdOrCtrl+V', selector: 'paste:' },
        {
          label: 'Select All',
          accelerator: 'CmdOrCtrl+A',
          selector: 'selectAll:',
        },
      ],
    }
  }

  setIsAuthenticated(isAuthenticated) {
    this.options = this.options || {}
    this.options.isAuthenticated = isAuthenticated
    const viewMenu = this.menu.items[2].submenu
    viewMenu.items[0].enabled = isAuthenticated // Tasks
    viewMenu.items[1].enabled = isAuthenticated // Filters
  }

  getViewMenu() {
    const self = this
    return {
      label: 'View',
      submenu: [
        {
          label: 'Tasks',
          accelerator: 'CmdOrCtrl+1',
          click() { self.emit('tasks') },
          enabled: this.options.isAuthenticated,
        },
        {
          label: 'Filters',
          accelerator: 'CmdOrCtrl+2',
          click() { self.emit('filters') },
          enabled: this.options.isAuthenticated,
        },
        {
          label: 'Authentication',
          accelerator: 'CmdOrCtrl+3',
          click() { self.emit('authenticate') },
        },
      ],
    }
  }

  getToolsMenu() {
    return {
      label: 'Tools',
      submenu: [
        {
          label: 'Developer Tools',
          accelerator: `CmdOrCtrl+${this.altOrOption}+I`,
          click(item, win) {
            if (win) {
              win.webContents.toggleDevTools()
            }
          },
        },
      ],
    }
  }

  getAppMenu() {
    return {
      label: app.getName(),
      submenu: [
        this.aboutOption,
        // { label: 'About', selector: 'orderFrontStandardAboutPanel:' },
        { type: 'separator' },
        {
          label: 'Quit',
          accelerator: 'Command+Q',
          click() { app.quit() },
        },
      ],
    }
  }

  buildOSXMenu() {
    this.template.push(this.getAppMenu())
    this.template.push(this.getEditMenu())
    this.template.push(this.getViewMenu())
    this.template.push(this.getToolsMenu())
    this.template.push({
      label: 'Help',
      role: 'help',
      submenu: [
        this.bugReportOption,
      ],
    })
  }

  getFileMenu() {
    return {
      label: 'File',
      submenu: [
        {
          label: 'Exit',
          accelerator: 'Ctrl+Q',
          click() { app.quit() },
        },
      ],
    }
  }

  buildNonOSXMenu() {
    this.template.push(this.getFileMenu())
    this.template.push(this.getEditMenu())
    this.template.push(this.getViewMenu())
    this.template.push(this.getToolsMenu())
    this.template.push({
      label: 'Help',
      submenu: [
        this.aboutOption,
        this.bugReportOption,
      ],
    })
  }
}

module.exports = AppMenu
<|endoftext|>


<commit_before>var q = require('q');

var _ = require('lodash');

var mongoose = require('mongoose');
var findOrCreate = require('mongoose-findorcreate');

var postModel = require('../models/post');

var logger = require('../utils/logger');

var socialSchema = new mongoose.Schema({
    type: String,
    id: String
});

var profileSchema = new mongoose.Schema({
    createdAt: { type: Date, default: Date.now },
    updatedAt: { type: Date, default: Date.now },
    name: String,
    avatar: String,
    social: [socialSchema],
    locale: { type: String, default: 'en-us' }
});

profileSchema.plugin(findOrCreate);

profileSchema.statics.createProfile = function (type, data) {

    var deferred = new q.defer();

    this.findOrCreate(
        { 'social.type': type, 'social.id': data.id },
        {
            name: data.name,
            avatar: data.avatar,
            social: [ { type: type, id: data.id } ]
        },
        { upsert: true },
        function (err, profile) {

            if (err || !profile) {

                logger.err('Error saving profile.', err, data);

                deferred.reject({
                    status: 500,
                    message: 'Internal Server Error'
                });

            } else { deferred.resolve(profile); }

        }
    );

    return deferred.promise;

};

profileSchema.statics.updateProfileById = function (profileId, data) {

    var deferred = new q.defer();

    this.findById(profileId)
        .exec(function (err, profile) {

            if (data.locale) {

                profile.locale = data.locale;

            }

            profile.save(function (err, profile) {

                if (err || !profile) {

                    logger.err('Error saving profile ' + profileId + '.', err, data);

                    deferred.reject({
                        status: 500,
                        message: 'Internal Server Error'
                    });

                } else { deferred.resolve(profile); }

            });

        });

    return deferred.promise;

};

profileSchema.statics.showProfileById = function (profileId) {

    var deferred = new q.defer();

    this.findById(profileId)
        .lean()
        .exec(function (err, profile) {

            if (err || !profile) {

                logger.err('Profile id ' + profileId + ' not found.');

                deferred.reject({
                    status: 404,
                    message: 'Profile not found.'
                });

            } else {

                postModel.find({ createdBy: profileId })
                    .populate('createdBy')
                    .sort({ createdAt: -1 })
                    .select('createdAt updatedAt views title slug contents messageCount createdBy')
                    .exec(function (err, posts) {

                        if (err || !posts) {

                            logger.err('Error retriving post for profile id ' + profileId + '.');

                            deferred.reject({
                                status: 500,
                                message: 'Error retriving profile posts.'
                            });

                        } else { deferred.resolve(_.assign(profile, { posts: posts })); }

                    });

            }

        });

    return deferred.promise;

};

profileSchema.pre('save', function (next) {

    this.updatedAt = Date.now();

    next();

});

module.exports = mongoose.model('profile', profileSchema);
<commit_msg>Corrected error status message.<commit_after>var q = require('q');

var _ = require('lodash');

var mongoose = require('mongoose');
var findOrCreate = require('mongoose-findorcreate');

var postModel = require('../models/post');

var logger = require('../utils/logger');

var socialSchema = new mongoose.Schema({
    type: String,
    id: String
});

var profileSchema = new mongoose.Schema({
    createdAt: { type: Date, default: Date.now },
    updatedAt: { type: Date, default: Date.now },
    name: String,
    avatar: String,
    social: [socialSchema],
    locale: { type: String, default: 'en-us' }
});

profileSchema.plugin(findOrCreate);

profileSchema.statics.createProfile = function (type, data) {

    var deferred = new q.defer();

    this.findOrCreate(
        { 'social.type': type, 'social.id': data.id },
        {
            name: data.name,
            avatar: data.avatar,
            social: [ { type: type, id: data.id } ]
        },
        { upsert: true },
        function (err, profile) {

            if (err || !profile) {

                logger.err('Error saving profile.', err, data);

                deferred.reject({
                    status: 500,
                    message: 'Internal Server Error'
                });

            } else { deferred.resolve(profile); }

        }
    );

    return deferred.promise;

};

profileSchema.statics.updateProfileById = function (profileId, data) {

    var deferred = new q.defer();

    this.findById(profileId)
        .exec(function (err, profile) {

            if (data.locale) {

                profile.locale = data.locale;

            }

            profile.save(function (err, profile) {

                if (err || !profile) {

                    logger.err('Error saving profile ' + profileId + '.', err, data);

                    deferred.reject({
                        status: 500,
                        message: 'Internal Server Error'
                    });

                } else { deferred.resolve(profile); }

            });

        });

    return deferred.promise;

};

profileSchema.statics.showProfileById = function (profileId) {

    var deferred = new q.defer();

    this.findById(profileId)
        .lean()
        .exec(function (err, profile) {

            if (err || !profile) {

                logger.err('Profile id ' + profileId + ' not found.');

                deferred.reject({
                    status: 404,
                    message: 'Profile not found.'
                });

            } else {

                postModel.find({ createdBy: profileId })
                    .populate('createdBy')
                    .sort({ createdAt: -1 })
                    .select('createdAt updatedAt views title slug contents messageCount createdBy')
                    .exec(function (err, posts) {

                        if (err || !posts) {

                            logger.err('Error retriving post for profile id ' + profileId + '.');

                            deferred.reject({
                                status: 500,
                                message: 'Internal Server Error'
                            });

                        } else { deferred.resolve(_.assign(profile, { posts: posts })); }

                    });

            }

        });

    return deferred.promise;

};

profileSchema.pre('save', function (next) {

    this.updatedAt = Date.now();

    next();

});

module.exports = mongoose.model('profile', profileSchema);
<|endoftext|>


<commit_before>import clone from 'lodash/lang/clone'
import contains from 'lodash/collection/contains'
import filter from 'lodash/collection/filter'
import find from 'lodash/collection/find'
import findIndex from 'lodash/array/findIndex'
import findKey from 'lodash/object/findKey'
import findWarnings from '../helpers/find-course-warnings'
import flatten from 'lodash/array/flatten'
import identity from 'lodash/utility/identity'
import isNumber from 'lodash/lang/isNumber'
import isTrue from '../helpers/is-true'
import isUndefined from 'lodash/lang/isUndefined'
import omit from 'lodash/object/omit'
import pluck from 'lodash/collection/pluck'
import present from 'present'
import reject from 'lodash/collection/reject'
import round from 'lodash/math/round'
import some from 'lodash/collection/some'
import stringify from 'json-stable-stringify'
import {v4 as uuid} from 'uuid'
const debug = require('debug')('gb:models')

import randomChar from '../helpers/random-char'

const now = new Date()

export default function Student(data) {
	const startTime = present()

	const baseStudent = {
		id: uuid(),
		name: 'Student ' + randomChar(),
		version: VERSION,

		creditsNeeded: 35,

		matriculation: now.getFullYear() - 2,
		graduation: now.getFullYear() + 2,
		advisor: '',

		dateLastModified: new Date(),
		dateCreated: new Date(),

		studies: [],
		schedules: {},
		overrides: {},
		fabrications: {},

		settings: {},

		graduatability: Promise.resolve({
			canGraduate: false,
			studyResults: [],
		}),

		toJSON() {
			return omit(this, value => value instanceof Promise)
		},
	}

	const student = {
		...baseStudent,
		...data,
	}

	debug(`Student(): it took ${round(present() - startTime, 2)} ms to make a student`)

	return student
}

////////
////////
////////

export function changeStudentName(student, newName) {
	return {...student, name: newName}
}
export function changeStudentAdvisor(student, newAdvisor) {
	return {...student, advisor: newAdvisor}
}
export function changeStudentCreditsNeeded(student, newCreditsNeeded) {
	return {...student, creditsNeeded: newCreditsNeeded}
}
export function changeStudentMatriculation(student, newMatriculation) {
	return {...student, matriculation: newMatriculation}
}
export function changeStudentGraduation(student, newGraduation) {
	return {...student, graduation: newGraduation}
}
export function changeStudentSetting(student, newSetting) {
	return {...student, setting: newSetting}
}

export function addScheduleToStudent(student, newSchedule) {
	return {...student, schedules: {...student.schedules, [newSchedule.id]: newSchedule}}
}

export function destroyScheduleFromStudent(student, scheduleId) {
	debug(`Student.destroySchedule(): removing schedule ${scheduleId}`)

	const deadSched = student.schedules[scheduleId]
	const schedules = omit(student.schedules, [scheduleId])

	if (deadSched && deadSched.active) {
		const otherSchedKey = findKey(schedules, sched =>
			sched.year === deadSched.year &&
			sched.semester === deadSched.semester &&
			sched.id !== deadSched.id)

		if (otherSchedKey) {
			schedules[otherSchedKey] = {...schedules[otherSchedKey], active: true}
		}
	}

	return {...student, schedules}
}

export function addCourseToSchedule(student, scheduleId, clbid) {
	if (!isNumber(clbid)) {
		throw new TypeError('addCourse(): clbid must be a number')
	}

	let schedule = clone(find(student.schedules, {id: scheduleId}))

	if (!schedule || contains(schedule.clbids, clbid)) {
		return student
	}

	debug(`adding clbid ${clbid} to schedule ${schedule.id} (${schedule.year}-${schedule.semester}.${schedule.index})`)

	schedule.clbids = schedule.clbids.concat(clbid)
	schedule.courses = getCourses(schedule.clbids, {year: schedule.year, semester: schedule.semester})

	return {...student, schedules: {...student.schedules, [schedule.id]: schedule}}
}

export function removeCourseFromSchedule(student, scheduleId, clbid) {
	if (!isNumber(clbid)) {
		throw new TypeError('removeCourse(): clbid must be a number')
	}

	let schedule = clone(find(student.schedules, {id: scheduleId}))

	debug(`removing clbid ${clbid} from schedule ${schedule.id} (${schedule.year}-${schedule.semester}.${schedule.index})`)

	schedule.clbids = reject(schedule.clbids, id => id === clbid)
	schedule.courses = getCourses(schedule.clbids)

	return {...student, schedules: {...student.schedules, [schedule.id]: schedule}}
}

export function moveCourseToSchedule(student, fromScheduleId, toScheduleId, clbid) {
	debug(`Student.moveCourse(): moving ${clbid} from schedule ${fromScheduleId} to schedule ${toScheduleId}`)

	student = removeCourseFromSchedule(student, fromScheduleId, clbid)
	student = addCourseToSchedule(student, toScheduleId, clbid)

	return {...student}
}

export function addAreaToStudent(student, areaOfStudy) {
	return {...student, studies: [...student.studies, areaOfStudy]}
}

export function removeAreaFromStudent(student, areaPath) {
	return {...student, studies: reject([...student.studies], {path: areaPath})}
}

export function setOverrideOnStudent(student, key, value) {
	let overrides = {...student.overrides}
	overrides[key] = value
	return {...student, overrides}
}

export function removeOverrideFromStudent(student, key) {
	let overrides = {...student.overrides}
	delete overrides[key]
	return {...student, overrides}
}

export function addFabricationToStudent(student, fabrication) {
	let fabrications = {...student.fabrications, [fabrication.id]: fabrication}
	return {...student, fabrications}
}

export function removeFabricationFromStudent(student, fabricationId) {
	let fabrications = {...student.fabrications}
	delete fabrications[fabricationId]
	return {...student, fabrications}
}

export function encodeStudent(student) {
	return encodeURIComponent(stringify(student))
}

export function saveStudent(student) {
	// grab the old (still JSON-encoded) student from localstorage
	// compare it to the current one
	// if they're different, update dateLastModified, stringify, and save.
	const oldVersion = localStorage.getItem(student.id)

	if (oldVersion !== stringify(student)) {
		debug(`saving student ${student.name} (${student.id})`)
		const student = student.dateLastModified = new Date()
		localStorage.setItem(student.id, stringify(student))
	}
}

export function moveScheduleInStudent(student, scheduleId, {year, semester}={}) {
	if (year === undefined && semester === undefined) {
		return student
	}

	let schedule = clone(find(student.schedules, {id: scheduleId}))

	if (typeof year === 'number') {
		schedule.year = year
	}
	if (typeof semester === 'number') {
		schedule.semester = semester
	}

	return {...student, schedules: {...student.schedules, [schedule.id]: schedule}}
}

export function reorderScheduleInStudent(student, scheduleId, index) {
	let schedule = {
		...find(student.schedules, {id: scheduleId}),
		index: index,
	}
	return {...student, schedules: {...student.schedules, [schedule.id]: schedule}}
}

export function renameScheduleInStudent(student, scheduleId, title) {
	let schedule = {
		...find(student.schedules, {id: scheduleId}),
		title: title,
	}
	return {...student, schedules: {...student.schedules, [schedule.id]: schedule}}
}

export function reorderCourseInSchedule(student, scheduleId, {clbid, index}) {
	if (!isNumber(clbid)) {
		throw new TypeError('reorderCourse(): clbid must be a number')
	}

	let schedule = clone(find(student.schedules, {id: scheduleId}))

	const oldIndex = findIndex(schedule.clbids, id => id === clbid)

	schedule.clbids = [...schedule.clbids]
	schedule.clbids.splice(oldIndex, 1)
	schedule.clbids.splice(index, 0, clbid)

	schedule.courses = getCourses(schedule.clbids, {year: schedule.year, semester: schedule.semester})

	return {...student, schedules: {...student.schedules, [schedule.id]: schedule}}
}

export function validateSchedule(schedule) {
	// Checks to see if the schedule is valid
	return schedule.courses.then(courses => {
		// only check the courses that have data
		courses = reject(courses, isUndefined)

		// Step one: do any times conflict?
		const conflicts = findWarnings(courses, schedule)

		const flattened = flatten(conflicts)
		const filtered = filter(flattened, identity)
		const warnings = pluck(filtered, 'warning')
		const hasConflict = some(warnings, isTrue)

		if (hasConflict) {
			debug('schedule conflicts', conflicts, hasConflict)
		}

		return {hasConflict, conflicts}
	})
}
<commit_msg>add some checks to throw if things can't find a schedule<commit_after>import clone from 'lodash/lang/clone'
import contains from 'lodash/collection/contains'
import filter from 'lodash/collection/filter'
import find from 'lodash/collection/find'
import findIndex from 'lodash/array/findIndex'
import findKey from 'lodash/object/findKey'
import findWarnings from '../helpers/find-course-warnings'
import flatten from 'lodash/array/flatten'
import identity from 'lodash/utility/identity'
import isNumber from 'lodash/lang/isNumber'
import isTrue from '../helpers/is-true'
import isUndefined from 'lodash/lang/isUndefined'
import omit from 'lodash/object/omit'
import pluck from 'lodash/collection/pluck'
import present from 'present'
import reject from 'lodash/collection/reject'
import round from 'lodash/math/round'
import some from 'lodash/collection/some'
import stringify from 'json-stable-stringify'
import {v4 as uuid} from 'uuid'
const debug = require('debug')('gb:models')

import randomChar from '../helpers/random-char'

const now = new Date()

export default function Student(data) {
	const startTime = present()

	const baseStudent = {
		id: uuid(),
		name: 'Student ' + randomChar(),
		version: VERSION,

		creditsNeeded: 35,

		matriculation: now.getFullYear() - 2,
		graduation: now.getFullYear() + 2,
		advisor: '',

		dateLastModified: new Date(),
		dateCreated: new Date(),

		studies: [],
		schedules: {},
		overrides: {},
		fabrications: {},

		settings: {},

		graduatability: Promise.resolve({
			canGraduate: false,
			studyResults: [],
		}),

		toJSON() {
			return omit(this, value => value instanceof Promise)
		},
	}

	const student = {
		...baseStudent,
		...data,
	}

	debug(`Student(): it took ${round(present() - startTime, 2)} ms to make a student`)

	return student
}

////////
////////
////////

export function changeStudentName(student, newName) {
	return {...student, name: newName}
}
export function changeStudentAdvisor(student, newAdvisor) {
	return {...student, advisor: newAdvisor}
}
export function changeStudentCreditsNeeded(student, newCreditsNeeded) {
	return {...student, creditsNeeded: newCreditsNeeded}
}
export function changeStudentMatriculation(student, newMatriculation) {
	return {...student, matriculation: newMatriculation}
}
export function changeStudentGraduation(student, newGraduation) {
	return {...student, graduation: newGraduation}
}
export function changeStudentSetting(student, newSetting) {
	return {...student, setting: newSetting}
}

export function addScheduleToStudent(student, newSchedule) {
	return {...student, schedules: {...student.schedules, [newSchedule.id]: newSchedule}}
}

export function destroyScheduleFromStudent(student, scheduleId) {
	debug(`Student.destroySchedule(): removing schedule ${scheduleId}`)

	const deadSched = student.schedules[scheduleId]
	const schedules = omit(student.schedules, [scheduleId])

	if (deadSched && deadSched.active) {
		const otherSchedKey = findKey(schedules, sched =>
			sched.year === deadSched.year &&
			sched.semester === deadSched.semester &&
			sched.id !== deadSched.id)

		if (otherSchedKey) {
			schedules[otherSchedKey] = {...schedules[otherSchedKey], active: true}
		}
	}

	return {...student, schedules}
}

export function addCourseToSchedule(student, scheduleId, clbid) {
	if (!isNumber(clbid)) {
		throw new TypeError('addCourse(): clbid must be a number')
	}

	let schedule = clone(find(student.schedules, {id: scheduleId}))
	if (!schedule) {
		throw new ReferenceError(`Could not find a schedule with an ID of ${scheduleId}.`)
	}

	if (contains(schedule.clbids, clbid)) {
		return student
	}

	debug(`adding clbid ${clbid} to schedule ${schedule.id} (${schedule.year}-${schedule.semester}.${schedule.index})`)

	schedule.clbids = schedule.clbids.concat(clbid)
	schedule.courses = getCourses(schedule.clbids, {year: schedule.year, semester: schedule.semester})

	return {...student, schedules: {...student.schedules, [schedule.id]: schedule}}
}

export function removeCourseFromSchedule(student, scheduleId, clbid) {
	if (!isNumber(clbid)) {
		throw new TypeError('removeCourse(): clbid must be a number')
	}

	let schedule = clone(find(student.schedules, {id: scheduleId}))

	if (!schedule) {
		throw new ReferenceError(`Could not find a schedule with an ID of ${scheduleId}.`)
	}

	if (!contains(schedule.clbids, clbid)) {
		return student
	}

	debug(`removing clbid ${clbid} from schedule ${schedule.id} (${schedule.year}-${schedule.semester}.${schedule.index})`)

	schedule.clbids = reject(schedule.clbids, id => id === clbid)
	schedule.courses = getCourses(schedule.clbids)

	return {...student, schedules: {...student.schedules, [schedule.id]: schedule}}
}

export function moveCourseToSchedule(student, fromScheduleId, toScheduleId, clbid) {
	debug(`Student.moveCourse(): moving ${clbid} from schedule ${fromScheduleId} to schedule ${toScheduleId}`)

	student = removeCourseFromSchedule(student, fromScheduleId, clbid)
	student = addCourseToSchedule(student, toScheduleId, clbid)

	return {...student}
}

export function addAreaToStudent(student, areaOfStudy) {
	return {...student, studies: [...student.studies, areaOfStudy]}
}

export function removeAreaFromStudent(student, areaPath) {
	return {...student, studies: reject([...student.studies], {path: areaPath})}
}

export function setOverrideOnStudent(student, key, value) {
	let overrides = {...student.overrides}
	overrides[key] = value
	return {...student, overrides}
}

export function removeOverrideFromStudent(student, key) {
	let overrides = {...student.overrides}
	delete overrides[key]
	return {...student, overrides}
}

export function addFabricationToStudent(student, fabrication) {
	let fabrications = {...student.fabrications, [fabrication.id]: fabrication}
	return {...student, fabrications}
}

export function removeFabricationFromStudent(student, fabricationId) {
	let fabrications = {...student.fabrications}
	delete fabrications[fabricationId]
	return {...student, fabrications}
}

export function encodeStudent(student) {
	return encodeURIComponent(stringify(student))
}

export function saveStudent(student) {
	// grab the old (still JSON-encoded) student from localstorage
	// compare it to the current one
	// if they're different, update dateLastModified, stringify, and save.
	const oldVersion = localStorage.getItem(student.id)

	if (oldVersion !== stringify(student)) {
		debug(`saving student ${student.name} (${student.id})`)
		const student = student.dateLastModified = new Date()
		localStorage.setItem(student.id, stringify(student))
	}
}

export function moveScheduleInStudent(student, scheduleId, {year, semester}={}) {
	if (year === undefined && semester === undefined) {
		return student
	}

	let schedule = clone(find(student.schedules, {id: scheduleId}))

	if (typeof year === 'number') {
		schedule.year = year
	}
	if (typeof semester === 'number') {
		schedule.semester = semester
	}

	return {...student, schedules: {...student.schedules, [schedule.id]: schedule}}
}

export function reorderScheduleInStudent(student, scheduleId, index) {
	let schedule = {
		...find(student.schedules, {id: scheduleId}),
		index: index,
	}
	return {...student, schedules: {...student.schedules, [schedule.id]: schedule}}
}

export function renameScheduleInStudent(student, scheduleId, title) {
	let schedule = {
		...find(student.schedules, {id: scheduleId}),
		title: title,
	}
	return {...student, schedules: {...student.schedules, [schedule.id]: schedule}}
}

export function reorderCourseInSchedule(student, scheduleId, {clbid, index}) {
	if (!isNumber(clbid)) {
		throw new TypeError('reorderCourse(): clbid must be a number')
	}

	let schedule = clone(find(student.schedules, {id: scheduleId}))

	const oldIndex = findIndex(schedule.clbids, id => id === clbid)

	schedule.clbids = [...schedule.clbids]
	schedule.clbids.splice(oldIndex, 1)
	schedule.clbids.splice(index, 0, clbid)

	schedule.courses = getCourses(schedule.clbids, {year: schedule.year, semester: schedule.semester})

	return {...student, schedules: {...student.schedules, [schedule.id]: schedule}}
}

export function validateSchedule(schedule) {
	// Checks to see if the schedule is valid
	return schedule.courses.then(courses => {
		// only check the courses that have data
		courses = reject(courses, isUndefined)

		// Step one: do any times conflict?
		const conflicts = findWarnings(courses, schedule)

		const flattened = flatten(conflicts)
		const filtered = filter(flattened, identity)
		const warnings = pluck(filtered, 'warning')
		const hasConflict = some(warnings, isTrue)

		if (hasConflict) {
			debug('schedule conflicts', conflicts, hasConflict)
		}

		return {hasConflict, conflicts}
	})
}
<|endoftext|>


<commit_before>function uploadFiles(url, files) {
    var formData = new FormData();
    var file = files[0];
    formData.append(file.name, file);
    formData.append('csrf_token', csrf_token[0].value);
    var xhr = new XMLHttpRequest();
    xhr.open('POST', url, true);
    xhr.onload = function(e) {
        if (xhr.readyState === 4) {
            if (xhr.status === 200) {
                showMessage(xhr.responseText, 'success', 8000);
                $('#delete').prop('disabled', false);
            } else {
                showMessage(xhr.statusText, 'warning', 8000);
                $('#delete').prop('disabled', true);
            }
        }
    };
    var progressBar = document.querySelector('progress');
    xhr.upload.onprogress = function(e) {
        if (e.lengthComputable) {
            progressBar.value = (e.loaded / e.total) * 100;
            progressBar.textContent = progressBar.value; // Fallback for unsupported browsers.
        }
    };
    xhr.send(formData); //multipart/form-data
}

$(document).ready(function() {
    $('input[type="file"]').get(0).addEventListener('change', function(e) {
        uploadFiles('upload', this.files);
    }, false);

    $('#delete').on('click', function (e) {
        var filename = document.querySelector('input[type="file"]').files[0].name;
        var xhr = new XMLHttpRequest();
        xhr.open('DELETE', 'delete', true);
        xhr.onload = function(e) {
            if (xhr.readyState === 4) {
                if (xhr.status === 200) {
                    showMessage(xhr.responseText, 'success', 8000);
                    $('#delete').prop('disabled', true);
                } else {
                    showMessage(xhr.statusText, 'warning', 8000);
                    $('#delete').prop('disabled', false);
                }
            }
        };
        xhr.send(filename);
    });
});
<commit_msg>Updates delete function to send csrf token to delete article.<commit_after>function uploadFiles(url, files) {
    var formData = new FormData();
    var file = files[0];
    formData.append(file.name, file);
    formData.append('csrf_token', csrf_token[0].value);
    var xhr = new XMLHttpRequest();
    xhr.open('POST', url, true);
    xhr.onload = function(e) {
        if (xhr.readyState === 4) {
            if (xhr.status === 200) {
                showMessage(xhr.responseText, 'success', 8000);
                $('#delete').prop('disabled', false);
            } else {
                showMessage(xhr.statusText, 'warning', 8000);
                $('#delete').prop('disabled', true);
            }
        }
    };
    var progressBar = document.querySelector('progress');
    xhr.upload.onprogress = function(e) {
        if (e.lengthComputable) {
            progressBar.value = (e.loaded / e.total) * 100;
            progressBar.textContent = progressBar.value; // Fallback for unsupported browsers.
        }
    };
    xhr.send(formData); //multipart/form-data
}

$(document).ready(function() {
    $('input[type="file"]').get(0).addEventListener('change', function(e) {
        uploadFiles('upload', this.files);
    }, false);

    $('#delete').on('click', function (e) {
        var xhr = new XMLHttpRequest();
        xhr.open('DELETE', 'delete', true);
        xhr.onload = function(e) {
            if (xhr.readyState === 4) {
                if (xhr.status === 200) {
                    showMessage(xhr.responseText, 'success', 8000);
                    $('#delete').prop('disabled', true);
                } else {
                    showMessage(xhr.statusText, 'warning', 8000);
                    $('#delete').prop('disabled', false);
                }
            }
        };
        xhr.send(csrf_token[0].value);
    });
});
<|endoftext|>


<commit_before>import React from 'react';
import InlineEdit from 'react-edit-inline';
import Ansible from '../../utils/Ansible';

var NameEdit = React.createClass({
  propTypes: {
    name: React.PropTypes.string,
    id: React.PropTypes.string
  },
  dataChange(data) {
    var x = new RegExp("^[A-Za-z][A-Za-z0-9]+$");
    if (x.test(data.name)) {
      Ansible.sendMessage('custom_names', {
        id: this.props.id,
        name: data.name
      });
    }
  },
  render() {
    return (

        <InlineEdit
          className="static"
          activeClassName="editing"
          text={this.props.name}
          change={this.dataChange}
          paramName="name"
          style = {{
            backgroundColor: 'white',
            minWidth: 150,
            display: 'inline-block',
            margin: 0,
            padding: 0,
            fontSize:15
          }}
        />

    );
  }
});

export default NameEdit;
<commit_msg>Emit error message on invalid name<commit_after>import React from 'react';
import InlineEdit from 'react-edit-inline';
import Ansible from '../../utils/Ansible';
import smalltalk from 'smalltalk';

var NameEdit = React.createClass({
  propTypes: {
    name: React.PropTypes.string,
    id: React.PropTypes.string
  },
  dataChange(data) {
    var x = new RegExp("^[A-Za-z][A-Za-z0-9]+$");
    if (x.test(data.name)) {
      Ansible.sendMessage('custom_names', {
        id: this.props.id,
        name: data.name
      });
    } else {
      smalltalk.alert(
        'Invalid Peripheral Name',
        'Names must be alphanumeric, and cannot contain spaces or start with a number.'
      ).then(()=>console.log('Ok.'), ()=>console.log('Cancel.'));
    }
  },
  render() {
    return (

        <InlineEdit
          className="static"
          activeClassName="editing"
          text={this.props.name}
          change={this.dataChange}
          paramName="name"
          style = {{
            backgroundColor: 'white',
            minWidth: 150,
            display: 'inline-block',
            margin: 0,
            padding: 0,
            fontSize:15
          }}
        />

    );
  }
});

export default NameEdit;
<|endoftext|>


<commit_before>/*    Copyright 2019 Firewalla INC
 *
 *    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/>.
 */

'use strict'

const log = require("./logger.js")(__filename);

const fs = require('fs');
const f = require('./Firewalla.js');
const cp = require('child_process');

const rclient = require('../util/redis_manager.js').getRedisClient()
const sclient = require('../util/redis_manager.js').getSubscriptionClient()
const pclient = require('../util/redis_manager.js').getPublishClient()

const dynamicConfigKey = "sys:features"

var dynamicConfigs = {}

let callbacks = {}

let config = null;
let userConfig = null;

const util = require('util');
const writeFileAsync = util.promisify(fs.writeFile);
const readFileAsync = util.promisify(fs.readFile);

async function updateUserConfig(updatedPart) {
  await getUserConfig(true);
  userConfig = Object.assign({}, userConfig, updatedPart);
  let userConfigFile = f.getUserConfigFolder() + "/config.json";
  await writeFileAsync(userConfigFile, JSON.stringify(userConfig, null, 2), 'utf8'); // pretty print
  getConfig(true);
}

function updateUserConfigSync(updatedPart) {
  getConfig(true);
  userConfig = Object.assign({}, userConfig, updatedPart);
  let userConfigFile = f.getUserConfigFolder() + "/config.json";
  fs.writeFileSync(userConfigFile, JSON.stringify(userConfig, null, 2), 'utf8'); // pretty print
  getConfig(true);
}

async function removeUserNetworkConfig() {
  await getUserConfig(true);

  delete userConfig.alternativeInterface;
  delete userConfig.secondaryInterface;
  delete userConfig.wifiInterface;
  delete userConfig.dhcpLeaseTime;

  let userConfigFile = f.getUserConfigFolder() + "/config.json";
  await writeFileAsync(userConfigFile, JSON.stringify(userConfig, null, 2), 'utf8'); // pretty print
}

async function getUserConfig(reload) {
  if (!userConfig || reload === true) {
    let userConfigFile = f.getUserConfigFolder() + "/config.json";
    userConfig = {};
    if (fs.existsSync(userConfigFile)) {
      userConfig = JSON.parse(await readFileAsync(userConfigFile, 'utf8'));
    }
  }
  return userConfig;
}

function getConfig(reload) {
  if(!config || reload === true) {
    let defaultConfig = JSON.parse(fs.readFileSync(f.getFirewallaHome() + "/net2/config.json", 'utf8'));
    let userConfigFile = f.getUserConfigFolder() + "/config.json";
    userConfig = {};
    for (let i = 0; i !== 5; i++) {
      try {
        if (fs.existsSync(userConfigFile)) {
          // let fileJson = fs.readFileSync(userConfigFile, 'utf8');
          // log.info(`getConfig fileJson:${fileJson}` + new Error("").stack);
          // userConfig = JSON.parse(fileJson);
          userConfig = JSON.parse(fs.readFileSync(userConfigFile, 'utf8'));
          break;
        }
      } catch (err) {
        log.error(`Error parsing user config, retry count ${i}`, err);
        cp.execSync('sleep 1');
      }
    }

    let testConfig = {};
    if(process.env.NODE_ENV === 'test') {
      let testConfigFile = f.getUserConfigFolder() + "/config.test.json";
      if(fs.existsSync(testConfigFile)) {
        testConfig = JSON.parse(fs.readFileSync(testConfigFile, 'utf8'));
        log.warn("Test config is being used", testConfig);
      }
    }

    // user config will override system default config file
    config = Object.assign({}, defaultConfig, userConfig, testConfig);
  }
  return config;
}

function isFeatureOn_Static(featureName) {
  let config = getConfig()
  if(config.userFeatures && featureName in config.userFeatures) {
    if(config.userFeatures[featureName]) {
      return true
    } else {
      return false
    }
  } else {
    return undefined
  }
}

// undefined: feature not exists
// true: feature enabled
// false: feature disabled
function isFeatureOn_Dynamic(featureName) {
  if(dynamicConfigs && featureName in dynamicConfigs) {
    if(dynamicConfigs[featureName] === '1' ) {
      return true
    } else {
      return false
    }
  } else {
    return undefined
  }
}

function isFeatureHidden(featureName) {
  if(!f.isProductionOrBeta()) {
    return false; // for dev mode, never hide features
  }

  const config = getConfig();
  if(config.hiddenFeatures &&
    Array.isArray(config.hiddenFeatures) &&
    config.hiddenFeatures.includes(featureName)) {
    return true;
  } else {
    return false;
  }
}

function isFeatureOn(featureName, defaultValue) {
  if(isFeatureHidden(featureName)) {
    return false;
  }

  const dynamicFlag = isFeatureOn_Dynamic(featureName)
  if(dynamicFlag !== undefined) {
    return dynamicFlag
  }

  const staticFlag = isFeatureOn_Static(featureName)
  if(staticFlag !== undefined) {
    return staticFlag
  } else {
    return defaultValue || false // default disabled
  }
}

async function syncDynamicFeaturesConfigs() {
  let configs = await rclient.hgetallAsync(dynamicConfigKey);
  if(configs) {
    dynamicConfigs = configs
  } else {
    dynamicConfigs = {}
  }
}

async function enableDynamicFeature(featureName) {
  await rclient.hsetAsync(dynamicConfigKey, featureName, '1');
  pclient.publish("config:feature:dynamic:enable", featureName)
  dynamicConfigs[featureName] = '1'
}

async function disableDynamicFeature(featureName) {
  await rclient.hsetAsync(dynamicConfigKey, featureName, '0');
  pclient.publish("config:feature:dynamic:disable", featureName)
  dynamicConfigs[featureName] = '0'
}

async function clearDynamicFeature(featureName) {
  await rclient.hdel(dynamicConfigKey, featureName);
  pclient.publish("config:feature:dynamic:clear", featureName)
  delete dynamicConfigs[featureName]
}

function getDynamicConfigs() {
  return dynamicConfigs
}

function getFeatures() {
  let staticFeatures = getConfig().userFeatures
  let dynamicFeatures = getDynamicConfigs()

  let x = {}

  let merged = Object.assign(x, staticFeatures, dynamicFeatures)

  for(let key in merged) {
    if(merged[key] == '0') {
      merged[key] = false
    }

    if(merged[key] == '1') {
      merged[key] = true
    }
  }

  const hiddenFeatures = getConfig().hiddenFeatures;

  if(f.isProductionOrBeta()) { // only apply hidden features for prod or beta
    if(hiddenFeatures && Array.isArray(hiddenFeatures)) {
      hiddenFeatures.forEach((f) => {
        if(f in merged) {
          delete merged[f];  // should be filtered out if hiddenFeatures contain the feature, this can force this feature not seen from app side
        }
      });
    }
  }

  return merged
}

sclient.subscribe("config:feature:dynamic:enable")
sclient.subscribe("config:feature:dynamic:disable")
sclient.subscribe("config:feature:dynamic:clear")

sclient.on("message", (channel, message) => {
  log.debug(`got message from ${channel}: ${message}`)
  const theFeature = message
  switch(channel) {
  case "config:feature:dynamic:enable":
    dynamicConfigs[theFeature] = '1'
    if(callbacks[theFeature]) {
      callbacks[theFeature].forEach((c) => {
        c(theFeature, true)
      })
    }
    break
  case "config:feature:dynamic:disable":
    dynamicConfigs[theFeature] = '0'
    if(callbacks[theFeature]) {
      callbacks[theFeature].forEach((c) => {
        c(theFeature, false)
      })
    }
    break
  case "config:feature:dynamic:clear":
    delete dynamicConfigs[theFeature]
    break
  }
});

syncDynamicFeaturesConfigs()

setInterval(() => {
  syncDynamicFeaturesConfigs()
}, 60 * 1000) // every minute

function onFeature(feature, callback) {
  if(!callbacks[feature]) {
    callbacks[feature] = []
  }

  callbacks[feature].push(callback)
}

function getTimingConfig(key) {
  const config = getConfig();
  return config && config.timing && config.timing[key];
}

function getSimpleVersion() {
  const hash = f.getLatestCommitHash();
  const version = getConfig() && getConfig().version;
  return `${version}-${hash}`;
}

module.exports = {
  updateUserConfig: updateUserConfig,
  updateUserConfigSync: updateUserConfigSync,
  getConfig: getConfig,
  getSimpleVersion: getSimpleVersion,
  getUserConfig: getUserConfig,
  getTimingConfig: getTimingConfig,
  isFeatureOn: isFeatureOn,
  getFeatures: getFeatures,
  getDynamicConfigs: getDynamicConfigs,
  enableDynamicFeature:enableDynamicFeature,
  disableDynamicFeature:disableDynamicFeature,
  clearDynamicFeature: clearDynamicFeature,
  syncDynamicFeaturesConfigs: syncDynamicFeaturesConfigs,
  onFeature: onFeature,
  removeUserNetworkConfig: removeUserNetworkConfig
};
<commit_msg>update<commit_after>/*    Copyright 2019 Firewalla INC
 *
 *    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/>.
 */

'use strict'

const log = require("./logger.js")(__filename);

const fs = require('fs');
const f = require('./Firewalla.js');
const cp = require('child_process');

const rclient = require('../util/redis_manager.js').getRedisClient()
const sclient = require('../util/redis_manager.js').getSubscriptionClient()
const pclient = require('../util/redis_manager.js').getPublishClient()

const dynamicConfigKey = "sys:features"

var dynamicConfigs = {}

let callbacks = {}

let config = null;
let userConfig = null;

const util = require('util');
const writeFileAsync = util.promisify(fs.writeFile);
const readFileAsync = util.promisify(fs.readFile);

async function updateUserConfig(updatedPart) {
  await getUserConfig(true);
  userConfig = Object.assign({}, userConfig, updatedPart);
  let userConfigFile = f.getUserConfigFolder() + "/config.json";
  await writeFileAsync(userConfigFile, JSON.stringify(userConfig, null, 2), 'utf8'); // pretty print
  getConfig(true);
}

function updateUserConfigSync(updatedPart) {
  getConfig(true);
  userConfig = Object.assign({}, userConfig, updatedPart);
  let userConfigFile = f.getUserConfigFolder() + "/config.json";
  fs.writeFileSync(userConfigFile, JSON.stringify(userConfig, null, 2), 'utf8'); // pretty print
  getConfig(true);
}

async function removeUserNetworkConfig() {
  await getUserConfig(true);

  delete userConfig.alternativeInterface;
  delete userConfig.secondaryInterface;
  delete userConfig.wifiInterface;
  delete userConfig.dhcpLeaseTime;

  let userConfigFile = f.getUserConfigFolder() + "/config.json";
  await writeFileAsync(userConfigFile, JSON.stringify(userConfig, null, 2), 'utf8'); // pretty print
}

async function getUserConfig(reload) {
  if (!userConfig || reload === true) {
    let userConfigFile = f.getUserConfigFolder() + "/config.json";
    userConfig = {};
    if (fs.existsSync(userConfigFile)) {
      userConfig = JSON.parse(await readFileAsync(userConfigFile, 'utf8'));
    }
  }
  return userConfig;
}

function getConfig(reload) {
  if(!config || reload === true) {
    let defaultConfig = JSON.parse(fs.readFileSync(f.getFirewallaHome() + "/net2/config.json", 'utf8'));
    let userConfigFile = f.getUserConfigFolder() + "/config.json";
    userConfig = {};
    for (let i = 0; i !== 5; i++) {
      try {
        if (fs.existsSync(userConfigFile)) {
          // let fileJson = fs.readFileSync(userConfigFile, 'utf8');
          // log.info(`getConfig fileJson:${fileJson}` + new Error("").stack);
          // userConfig = JSON.parse(fileJson);
          userConfig = JSON.parse(fs.readFileSync(userConfigFile, 'utf8'));
          break;
        }
      } catch (err) {
        log.error(`Error parsing user config, retry count ${i}`, err);
        cp.execSync('sleep 1');
      }
    }

    let testConfig = {};
    if(process.env.NODE_ENV === 'test') {
      let testConfigFile = f.getUserConfigFolder() + "/config.test.json";
      if(fs.existsSync(testConfigFile)) {
        testConfig = JSON.parse(fs.readFileSync(testConfigFile, 'utf8'));
        log.warn("Test config is being used", testConfig);
      }
    }

    // user config will override system default config file
    config = Object.assign({}, defaultConfig, userConfig, testConfig);
  }
  return config;
}

function isFeatureOn_Static(featureName) {
  let config = getConfig()
  if(config.userFeatures && featureName in config.userFeatures) {
    if(config.userFeatures[featureName]) {
      return true
    } else {
      return false
    }
  } else {
    return undefined
  }
}

// undefined: feature not exists
// true: feature enabled
// false: feature disabled
function isFeatureOn_Dynamic(featureName) {
  if(dynamicConfigs && featureName in dynamicConfigs) {
    if(dynamicConfigs[featureName] === '1' ) {
      return true
    } else {
      return false
    }
  } else {
    return undefined
  }
}

function isFeatureHidden(featureName) {
  if(!f.isProductionOrBeta()) {
    return false; // for dev mode, never hide features
  }

  const config = getConfig();
  if(config.hiddenFeatures &&
    Array.isArray(config.hiddenFeatures) &&
    config.hiddenFeatures.includes(featureName)) {
    return true;
  } else {
    return false;
  }
}

function isFeatureOn(featureName, defaultValue) {
  // only enable smart_block on dev version temporarily
  if(featureName == 'smart_block' && !f.isDevelopmentVersion()){
    return false;
  }

  if(isFeatureHidden(featureName)) {
    return false;
  }

  const dynamicFlag = isFeatureOn_Dynamic(featureName)
  if(dynamicFlag !== undefined) {
    return dynamicFlag
  }

  const staticFlag = isFeatureOn_Static(featureName)
  if(staticFlag !== undefined) {
    return staticFlag
  } else {
    return defaultValue || false // default disabled
  }
}

async function syncDynamicFeaturesConfigs() {
  let configs = await rclient.hgetallAsync(dynamicConfigKey);
  if(configs) {
    dynamicConfigs = configs
  } else {
    dynamicConfigs = {}
  }
}

async function enableDynamicFeature(featureName) {
  await rclient.hsetAsync(dynamicConfigKey, featureName, '1');
  pclient.publish("config:feature:dynamic:enable", featureName)
  dynamicConfigs[featureName] = '1'
}

async function disableDynamicFeature(featureName) {
  await rclient.hsetAsync(dynamicConfigKey, featureName, '0');
  pclient.publish("config:feature:dynamic:disable", featureName)
  dynamicConfigs[featureName] = '0'
}

async function clearDynamicFeature(featureName) {
  await rclient.hdel(dynamicConfigKey, featureName);
  pclient.publish("config:feature:dynamic:clear", featureName)
  delete dynamicConfigs[featureName]
}

function getDynamicConfigs() {
  return dynamicConfigs
}

function getFeatures() {
  let staticFeatures = getConfig().userFeatures
  let dynamicFeatures = getDynamicConfigs()

  let x = {}

  let merged = Object.assign(x, staticFeatures, dynamicFeatures)

  for(let key in merged) {
    if(merged[key] == '0') {
      merged[key] = false
    }

    if(merged[key] == '1') {
      merged[key] = true
    }
  }

  const hiddenFeatures = getConfig().hiddenFeatures;

  if(f.isProductionOrBeta()) { // only apply hidden features for prod or beta
    if(hiddenFeatures && Array.isArray(hiddenFeatures)) {
      hiddenFeatures.forEach((f) => {
        if(f in merged) {
          delete merged[f];  // should be filtered out if hiddenFeatures contain the feature, this can force this feature not seen from app side
        }
      });
    }
  }

  return merged
}

sclient.subscribe("config:feature:dynamic:enable")
sclient.subscribe("config:feature:dynamic:disable")
sclient.subscribe("config:feature:dynamic:clear")

sclient.on("message", (channel, message) => {
  log.debug(`got message from ${channel}: ${message}`)
  const theFeature = message
  switch(channel) {
  case "config:feature:dynamic:enable":
    dynamicConfigs[theFeature] = '1'
    if(callbacks[theFeature]) {
      callbacks[theFeature].forEach((c) => {
        c(theFeature, true)
      })
    }
    break
  case "config:feature:dynamic:disable":
    dynamicConfigs[theFeature] = '0'
    if(callbacks[theFeature]) {
      callbacks[theFeature].forEach((c) => {
        c(theFeature, false)
      })
    }
    break
  case "config:feature:dynamic:clear":
    delete dynamicConfigs[theFeature]
    break
  }
});

syncDynamicFeaturesConfigs()

setInterval(() => {
  syncDynamicFeaturesConfigs()
}, 60 * 1000) // every minute

function onFeature(feature, callback) {
  if(!callbacks[feature]) {
    callbacks[feature] = []
  }

  callbacks[feature].push(callback)
}

function getTimingConfig(key) {
  const config = getConfig();
  return config && config.timing && config.timing[key];
}

function getSimpleVersion() {
  const hash = f.getLatestCommitHash();
  const version = getConfig() && getConfig().version;
  return `${version}-${hash}`;
}

module.exports = {
  updateUserConfig: updateUserConfig,
  updateUserConfigSync: updateUserConfigSync,
  getConfig: getConfig,
  getSimpleVersion: getSimpleVersion,
  getUserConfig: getUserConfig,
  getTimingConfig: getTimingConfig,
  isFeatureOn: isFeatureOn,
  getFeatures: getFeatures,
  getDynamicConfigs: getDynamicConfigs,
  enableDynamicFeature:enableDynamicFeature,
  disableDynamicFeature:disableDynamicFeature,
  clearDynamicFeature: clearDynamicFeature,
  syncDynamicFeaturesConfigs: syncDynamicFeaturesConfigs,
  onFeature: onFeature,
  removeUserNetworkConfig: removeUserNetworkConfig
};
<|endoftext|>