<commit_before>const getStackFamily = require('./stack-family');
const getChangeSetFamily = require('./change-set-family');
const deltaArrow = require('./delta-arrow');
const deltaValue = require('./delta-value');

/**
 * Returns a multi-line string describing the parameters that have changed
 * @param {ParameterDeltas} deltas
 * @returns {String}
 */
function parameterDeltasList(deltas) {
  if (!deltas.length) {
    return 'This change set contained no meaningful parameter deltas.';
  }

  // Text blocks within attachments have a 3000 character limit. If the text is
  // too large, try creating the list without links to reduce the size.
  const withLinks = deltas
    .map((d) => {
      const oldValue = deltaValue(d.parameter, d.stackValue);
      const arrow = deltaArrow(d);
      const newValue = deltaValue(d.parameter, d.changeSetValue);

      let label;

      if (
        [
          'infrastructure-cd-root-staging',
          'infrastructure-cd-root-production',
        ].includes(d.stackName)
      ) {
        // For root stack parameters, label with just the parameter name
        label = d.parameter;
      } else {
        // For nested stacks, try to extract some meaningful part of the stack
        // name that is useful to include
        const nickname = d.stackName.split('-').at(-2);
        label = `${nickname}::${d.parameter}`;
      }

      return `*${label}*: ${oldValue} ${arrow} ${newValue}`;
    })
    .join('\n');

  if (withLinks.length < 2900) {
    return withLinks;
  } else {
    return deltas
      .map((d) => {
        const oldValue = deltaValue(d.parameter, d.stackValue, true);
        const arrow = deltaArrow(d, true);
        const newValue = deltaValue(d.parameter, d.changeSetValue, true);

        return `*${d.stackName}::${d.parameter}*: ${oldValue} ${arrow} ${newValue}`;
      })
      .join('\n');
  }
}

module.exports = {
  async nestedParameterDeltaText(stackName, changeSetName) {
    let stackFamily = await getStackFamily(stackName);
    const changeSetFamily = await getChangeSetFamily(stackName, changeSetName);
    const changeSetFamilyStackIds = changeSetFamily.map((c) => c.StackId);

    // When there's only a single change set, it likely means that nested
    // change sets was disabled for the given change set. In these cases, only
    // the root stack's parameters should be included in the deltas, because
    // there won't be anything to compare the child stacks to, and all
    // parameters will look like they've changed.
    if (changeSetFamily.length === 1) {
      stackFamily = [stackFamily[0]];
    }

    /** @type {ParameterDeltas} */
    const deltas = [];

    // Iterate through all existing stack parameters and create a delta for
    // each one, which includes the current value.
    for (const stack of stackFamily) {
      // Not all nested stacks are guaranteed to be have a change set. If a
      // stack in the stack family does not have a corresponding change set,
      // and the deltas are determined strictly by finding matching values in
      // the stack and change set families, all parameters for that stack would
      // appear to be unmatched. So, if there is no corresponding change set
      // for a given stack, that stack's parameters should not be included in
      // the deltas. (If there is a change set with no corresponding stack,
      // those deltas *will* still be included below.)
      if (changeSetFamilyStackIds.includes(stack.StackId)) {
        for (const param of stack.Parameters || []) {
          deltas.push({
            stackName: stack.StackName,
            stackId: stack.StackId,
            parameter: param.ParameterKey,
            stackValue: param.ResolvedValue || param.ParameterValue,
            changeSetValue: null,
          });
        }
      }
    }

    // Iterate through all the change set parameters. If a delta already exists
    // for a given stack+parameter key, add the value from the change set to
    // that delta. If not, make a new delta.
    for (const changeSet of changeSetFamily) {
      for (const param of changeSet.Parameters || []) {
        const delta = deltas.find(
          (d) =>
            param.ParameterKey === d.parameter &&
            changeSet.StackName === d.stackName,
        );

        if (delta) {
          delta.changeSetValue = param.ResolvedValue || param.ParameterValue;
        } else {
          deltas.push({
            stackName: changeSet.StackName,
            stackId: changeSet.StackId,
            parameter: param.ParameterKey,
            stackValue: null,
            changeSetValue: param.ResolvedValue || param.ParameterValue,
          });
        }
      }
    }

    // Filter down to only deltas where the values are different
    const changeDeltas = deltas.filter(
      (d) => d.stackValue !== d.changeSetValue,
    );

    // When using nested change sets, not all parameters in nested stacks are
    // correctly resolved. Any parameter values that depend on a stack or
    // resource output will be included in the change set parameters with an
    // unresolved value that looks like "{{IntrinsicFunction:…". Unless or until
    // AWS makes nested change sets smarter, we have to just ignore these,
    // because there's no way to compare the actual existing value from the stack
    // to the hypothetical future value that will be resolved when the change set
    // executes.
    //
    // Any parameters effected by this limitation are filtered out.
    const cleanedDeltas = changeDeltas.filter(
      (d) => !(d.changeSetValue || '').match(/\{\{IntrinsicFunction\:/),
    );

    // Some additional parameters that don't make sense to display in Slack are
    // also filtered out.
    const allowedDeltas = cleanedDeltas.filter(
      (d) =>
        ![
          'PipelineExecutionNonce',
          'TemplateUrlBase',
          'TemplateUrlPrefix',
        ].includes(d.parameter),
    );

    console.log(JSON.stringify(allowedDeltas));
    return parameterDeltasList(allowedDeltas);
  },
};
<commit_msg>Update parameter labels<commit_after>const getStackFamily = require('./stack-family');
const getChangeSetFamily = require('./change-set-family');
const deltaArrow = require('./delta-arrow');
const deltaValue = require('./delta-value');

/**
 * Returns a multi-line string describing the parameters that have changed
 * @param {ParameterDeltas} deltas
 * @returns {String}
 */
function parameterDeltasList(deltas) {
  if (!deltas.length) {
    return 'This change set contained no meaningful parameter deltas.';
  }

  // Text blocks within attachments have a 3000 character limit. If the text is
  // too large, try creating the list without links to reduce the size.
  const withLinks = deltas
    .map((d) => {
      const oldValue = deltaValue(d.parameter, d.stackValue);
      const arrow = deltaArrow(d);
      const newValue = deltaValue(d.parameter, d.changeSetValue);

      let label;

      if (
        [
          'infrastructure-cd-root-staging',
          'infrastructure-cd-root-production',
        ].includes(d.stackName)
      ) {
        // For root stack parameters, label with just the parameter name
        label = d.parameter;
      } else {
        // For nested stacks, try to extract some meaningful part of the stack
        // name that is useful to include
        const nickname = d.stackName.split('-').at(-2);
        label = `${nickname}::${d.parameter}`;
      }

      return `*${label}*: ${oldValue} ${arrow} ${newValue}`;
    })
    .join('\n');

  if (withLinks.length < 2900) {
    return withLinks;
  } else {
    return deltas
      .map((d) => {
        const oldValue = deltaValue(d.parameter, d.stackValue, true);
        const arrow = deltaArrow(d, true);
        const newValue = deltaValue(d.parameter, d.changeSetValue, true);

        let label;

        if (
          [
            'infrastructure-cd-root-staging',
            'infrastructure-cd-root-production',
          ].includes(d.stackName)
        ) {
          // For root stack parameters, label with just the parameter name
          label = d.parameter;
        } else {
          // For nested stacks, try to extract some meaningful part of the stack
          // name that is useful to include
          const nickname = d.stackName.split('-').at(-2);
          label = `${nickname}::${d.parameter}`;
        }

        return `*${label}*: ${oldValue} ${arrow} ${newValue}`;
      })
      .join('\n');
  }
}

module.exports = {
  async nestedParameterDeltaText(stackName, changeSetName) {
    let stackFamily = await getStackFamily(stackName);
    const changeSetFamily = await getChangeSetFamily(stackName, changeSetName);
    const changeSetFamilyStackIds = changeSetFamily.map((c) => c.StackId);

    // When there's only a single change set, it likely means that nested
    // change sets was disabled for the given change set. In these cases, only
    // the root stack's parameters should be included in the deltas, because
    // there won't be anything to compare the child stacks to, and all
    // parameters will look like they've changed.
    if (changeSetFamily.length === 1) {
      stackFamily = [stackFamily[0]];
    }

    /** @type {ParameterDeltas} */
    const deltas = [];

    // Iterate through all existing stack parameters and create a delta for
    // each one, which includes the current value.
    for (const stack of stackFamily) {
      // Not all nested stacks are guaranteed to be have a change set. If a
      // stack in the stack family does not have a corresponding change set,
      // and the deltas are determined strictly by finding matching values in
      // the stack and change set families, all parameters for that stack would
      // appear to be unmatched. So, if there is no corresponding change set
      // for a given stack, that stack's parameters should not be included in
      // the deltas. (If there is a change set with no corresponding stack,
      // those deltas *will* still be included below.)
      if (changeSetFamilyStackIds.includes(stack.StackId)) {
        for (const param of stack.Parameters || []) {
          deltas.push({
            stackName: stack.StackName,
            stackId: stack.StackId,
            parameter: param.ParameterKey,
            stackValue: param.ResolvedValue || param.ParameterValue,
            changeSetValue: null,
          });
        }
      }
    }

    // Iterate through all the change set parameters. If a delta already exists
    // for a given stack+parameter key, add the value from the change set to
    // that delta. If not, make a new delta.
    for (const changeSet of changeSetFamily) {
      for (const param of changeSet.Parameters || []) {
        const delta = deltas.find(
          (d) =>
            param.ParameterKey === d.parameter &&
            changeSet.StackName === d.stackName,
        );

        if (delta) {
          delta.changeSetValue = param.ResolvedValue || param.ParameterValue;
        } else {
          deltas.push({
            stackName: changeSet.StackName,
            stackId: changeSet.StackId,
            parameter: param.ParameterKey,
            stackValue: null,
            changeSetValue: param.ResolvedValue || param.ParameterValue,
          });
        }
      }
    }

    // Filter down to only deltas where the values are different
    const changeDeltas = deltas.filter(
      (d) => d.stackValue !== d.changeSetValue,
    );

    // When using nested change sets, not all parameters in nested stacks are
    // correctly resolved. Any parameter values that depend on a stack or
    // resource output will be included in the change set parameters with an
    // unresolved value that looks like "{{IntrinsicFunction:…". Unless or until
    // AWS makes nested change sets smarter, we have to just ignore these,
    // because there's no way to compare the actual existing value from the stack
    // to the hypothetical future value that will be resolved when the change set
    // executes.
    //
    // Any parameters effected by this limitation are filtered out.
    const cleanedDeltas = changeDeltas.filter(
      (d) => !(d.changeSetValue || '').match(/\{\{IntrinsicFunction\:/),
    );

    // Some additional parameters that don't make sense to display in Slack are
    // also filtered out.
    const allowedDeltas = cleanedDeltas.filter(
      (d) =>
        ![
          'PipelineExecutionNonce',
          'TemplateUrlBase',
          'TemplateUrlPrefix',
        ].includes(d.parameter),
    );

    console.log(JSON.stringify(allowedDeltas));
    return parameterDeltasList(allowedDeltas);
  },
};
<|endoftext|>


<commit_before>const getStackFamily = require('./stack-family');
const getChangeSetFamily = require('./change-set-family');
const deltaArrow = require('./delta-arrow');
const deltaValue = require('./delta-value');

/**
 * Returns a multi-line string describing the parameters that have changed
 * @param {ParameterDeltas} deltas
 * @returns {String}
 */
function parameterDeltasList(deltas) {
  if (!deltas.length) {
    return 'This change set contained no meaningful parameter deltas.';
  }

  // Text blocks within attachments have a 3000 character limit. If the text is
  // too large, try creating the list without links to reduce the size.
  const withLinks = deltas
    .map((d) => {
      const oldValue = deltaValue(d.parameter, d.stackValue);
      const arrow = deltaArrow(d);
      const newValue = deltaValue(d.parameter, d.changeSetValue);

      let label;

      if (
        [
          'infrastructure-cd-root-staging',
          'infrastructure-cd-root-production',
        ].includes(d.stackName)
      ) {
        // For root stack parameters, label with just the parameter name
        label = d.parameter;
      } else {
        // For nested stacks, try to extract some meaningful part of the stack
        // name that is useful to include
        const nickname = d.stackName.split('-').at(-2);
        label = `${nickname}::${d.parameter}`;
      }

      return `*${label}*: ${oldValue} ${arrow} ${newValue}`;
    })
    .join('\n');

  if (withLinks.length < 2900) {
    return withLinks;
  } else {
    return deltas
      .map((d) => {
        const oldValue = deltaValue(d.parameter, d.stackValue, true);
        const arrow = deltaArrow(d, true);
        const newValue = deltaValue(d.parameter, d.changeSetValue, true);

        let label;

        if (
          [
            'infrastructure-cd-root-staging',
            'infrastructure-cd-root-production',
          ].includes(d.stackName)
        ) {
          // For root stack parameters, label with just the parameter name
          label = d.parameter;
        } else {
          // For nested stacks, try to extract some meaningful part of the stack
          // name that is useful to include
          const nickname = d.stackName.split('-').at(-2);
          label = `${nickname}::${d.parameter}`;
        }

        return `*${label}*: ${oldValue} ${arrow} ${newValue}`;
      })
      .join('\n');
  }
}

module.exports = {
  async nestedParameterDeltaText(stackName, changeSetName) {
    let stackFamily = await getStackFamily(stackName);
    const changeSetFamily = await getChangeSetFamily(stackName, changeSetName);
    const changeSetFamilyStackIds = changeSetFamily.map((c) => c.StackId);

    // When there's only a single change set, it likely means that nested
    // change sets was disabled for the given change set. In these cases, only
    // the root stack's parameters should be included in the deltas, because
    // there won't be anything to compare the child stacks to, and all
    // parameters will look like they've changed.
    if (changeSetFamily.length === 1) {
      stackFamily = [stackFamily[0]];
    }

    /** @type {ParameterDeltas} */
    const deltas = [];

    // Iterate through all existing stack parameters and create a delta for
    // each one, which includes the current value.
    for (const stack of stackFamily) {
      // Not all nested stacks are guaranteed to be have a change set. If a
      // stack in the stack family does not have a corresponding change set,
      // and the deltas are determined strictly by finding matching values in
      // the stack and change set families, all parameters for that stack would
      // appear to be unmatched. So, if there is no corresponding change set
      // for a given stack, that stack's parameters should not be included in
      // the deltas. (If there is a change set with no corresponding stack,
      // those deltas *will* still be included below.)
      if (changeSetFamilyStackIds.includes(stack.StackId)) {
        for (const param of stack.Parameters || []) {
          deltas.push({
            stackName: stack.StackName,
            stackId: stack.StackId,
            parameter: param.ParameterKey,
            stackValue: param.ResolvedValue || param.ParameterValue,
            changeSetValue: null,
          });
        }
      }
    }

    // Iterate through all the change set parameters. If a delta already exists
    // for a given stack+parameter key, add the value from the change set to
    // that delta. If not, make a new delta.
    for (const changeSet of changeSetFamily) {
      for (const param of changeSet.Parameters || []) {
        const delta = deltas.find(
          (d) =>
            param.ParameterKey === d.parameter &&
            changeSet.StackName === d.stackName,
        );

        if (delta) {
          delta.changeSetValue = param.ResolvedValue || param.ParameterValue;
        } else {
          deltas.push({
            stackName: changeSet.StackName,
            stackId: changeSet.StackId,
            parameter: param.ParameterKey,
            stackValue: null,
            changeSetValue: param.ResolvedValue || param.ParameterValue,
          });
        }
      }
    }

    // Filter down to only deltas where the values are different
    const changeDeltas = deltas.filter(
      (d) => d.stackValue !== d.changeSetValue,
    );

    // When using nested change sets, not all parameters in nested stacks are
    // correctly resolved. Any parameter values that depend on a stack or
    // resource output will be included in the change set parameters with an
    // unresolved value that looks like "{{IntrinsicFunction:…". Unless or until
    // AWS makes nested change sets smarter, we have to just ignore these,
    // because there's no way to compare the actual existing value from the stack
    // to the hypothetical future value that will be resolved when the change set
    // executes.
    //
    // Any parameters effected by this limitation are filtered out.
    const cleanedDeltas = changeDeltas.filter(
      (d) => !(d.changeSetValue || '').match(/\{\{IntrinsicFunction\:/),
    );

    // Some additional parameters that don't make sense to display in Slack are
    // also filtered out.
    const allowedDeltas = cleanedDeltas.filter(
      (d) =>
        ![
          'PipelineExecutionNonce',
          'TemplateUrlBase',
          'TemplateUrlPrefix',
        ].includes(d.parameter),
    );

    console.log(JSON.stringify(allowedDeltas));
    return parameterDeltasList(allowedDeltas);
  },
};
<commit_msg>Refactor delta links<commit_after>const getStackFamily = require('./stack-family');
const getChangeSetFamily = require('./change-set-family');
const deltaArrow = require('./delta-arrow');
const deltaValue = require('./delta-value');

/**
 * Returns a multi-line string describing the parameters that have changed
 * @param {ParameterDeltas} deltas
 * @returns {String}
 */
function parameterDeltasList(deltas) {
  if (!deltas.length) {
    return 'This change set contained no meaningful parameter deltas.';
  }

  function listItem(delta, noLinks = false) {
    const oldValue = deltaValue(delta.parameter, delta.stackValue, noLinks);
    const arrow = deltaArrow(delta);
    const newValue = deltaValue(delta.parameter, delta.changeSetValue, noLinks);

    let label;

    if (
      [
        'infrastructure-cd-root-staging',
        'infrastructure-cd-root-production',
      ].includes(delta.stackName)
    ) {
      // For root stack parameters, label with just the parameter name
      label = delta.parameter;
    } else {
      // For nested stacks, try to extract some meaningful part of the stack
      // name that is useful to include
      const nickname = delta.stackName.split('-').at(-2);
      label = `${nickname}::${delta.parameter}`;
    }

    return `*${label}*: ${oldValue} ${arrow} ${newValue}`;
  }

  // Text blocks within attachments have a 3000 character limit. If the text is
  // too large, try creating the list without links to reduce the size.
  const withLinks = deltas.map((d) => listItem(d, false)).join('\n');

  if (withLinks.length < 2900) {
    return withLinks;
  } else {
    const withoutLinks = deltas.map((d) => listItem(d, true)).join('\n');

    if (withoutLinks.length < 2900) {
      return withoutLinks;
    } else {
      return withoutLinks.slice(0, 2900);
    }
  }
}

module.exports = {
  async nestedParameterDeltaText(stackName, changeSetName) {
    let stackFamily = await getStackFamily(stackName);
    const changeSetFamily = await getChangeSetFamily(stackName, changeSetName);
    const changeSetFamilyStackIds = changeSetFamily.map((c) => c.StackId);

    // When there's only a single change set, it likely means that nested
    // change sets was disabled for the given change set. In these cases, only
    // the root stack's parameters should be included in the deltas, because
    // there won't be anything to compare the child stacks to, and all
    // parameters will look like they've changed.
    if (changeSetFamily.length === 1) {
      stackFamily = [stackFamily[0]];
    }

    /** @type {ParameterDeltas} */
    const deltas = [];

    // Iterate through all existing stack parameters and create a delta for
    // each one, which includes the current value.
    for (const stack of stackFamily) {
      // Not all nested stacks are guaranteed to be have a change set. If a
      // stack in the stack family does not have a corresponding change set,
      // and the deltas are determined strictly by finding matching values in
      // the stack and change set families, all parameters for that stack would
      // appear to be unmatched. So, if there is no corresponding change set
      // for a given stack, that stack's parameters should not be included in
      // the deltas. (If there is a change set with no corresponding stack,
      // those deltas *will* still be included below.)
      if (changeSetFamilyStackIds.includes(stack.StackId)) {
        for (const param of stack.Parameters || []) {
          deltas.push({
            stackName: stack.StackName,
            stackId: stack.StackId,
            parameter: param.ParameterKey,
            stackValue: param.ResolvedValue || param.ParameterValue,
            changeSetValue: null,
          });
        }
      }
    }

    // Iterate through all the change set parameters. If a delta already exists
    // for a given stack+parameter key, add the value from the change set to
    // that delta. If not, make a new delta.
    for (const changeSet of changeSetFamily) {
      for (const param of changeSet.Parameters || []) {
        const delta = deltas.find(
          (d) =>
            param.ParameterKey === d.parameter &&
            changeSet.StackName === d.stackName,
        );

        if (delta) {
          delta.changeSetValue = param.ResolvedValue || param.ParameterValue;
        } else {
          deltas.push({
            stackName: changeSet.StackName,
            stackId: changeSet.StackId,
            parameter: param.ParameterKey,
            stackValue: null,
            changeSetValue: param.ResolvedValue || param.ParameterValue,
          });
        }
      }
    }

    // Filter down to only deltas where the values are different
    const changeDeltas = deltas.filter(
      (d) => d.stackValue !== d.changeSetValue,
    );

    // When using nested change sets, not all parameters in nested stacks are
    // correctly resolved. Any parameter values that depend on a stack or
    // resource output will be included in the change set parameters with an
    // unresolved value that looks like "{{IntrinsicFunction:…". Unless or until
    // AWS makes nested change sets smarter, we have to just ignore these,
    // because there's no way to compare the actual existing value from the stack
    // to the hypothetical future value that will be resolved when the change set
    // executes.
    //
    // Any parameters effected by this limitation are filtered out.
    const cleanedDeltas = changeDeltas.filter(
      (d) => !(d.changeSetValue || '').match(/\{\{IntrinsicFunction\:/),
    );

    // Some additional parameters that don't make sense to display in Slack are
    // also filtered out.
    const allowedDeltas = cleanedDeltas.filter(
      (d) =>
        ![
          'PipelineExecutionNonce',
          'TemplateUrlBase',
          'TemplateUrlPrefix',
        ].includes(d.parameter),
    );

    console.log(JSON.stringify(allowedDeltas));
    return parameterDeltasList(allowedDeltas);
  },
};
<|endoftext|>


<commit_before>/**
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.*
 */

/* global API */

/**
 * Model of a Rule loaded from the Rules Engine
 * @constructor
 * @param {Gateway} gateway - The remote gateway to which to talk
 * @param {RuleDescription?} desc - Description of the rule to load
 * @param {Function?} onUpdate - Listener for when update is called
 */
function Rule(gateway, desc, onUpdate) {
  this.gateway = gateway;
  this.onUpdate = onUpdate;

  if (desc) {
    this.id = desc.id;
    this.enabled = desc.enabled;
    if (desc.name) {
      this.name = desc.name;
    } else {
      this.name = 'Rule Name';
    }
    this.trigger = desc.trigger;
    this.effect = desc.effect;
  } else {
    this.enabled = true;
  }
}

/**
 * Validate and save the rule
 * @return {Promise}
 */
Rule.prototype.update = function() {
  if (this.onUpdate) {
    this.onUpdate();
  }
  let desc = this.toDescription();
  if (!desc) {
    return Promise.reject('invalid description');
  }

  let fetchOptions = {
    headers: API.headers(),
    method: 'PUT',
    body: JSON.stringify(desc)
  };
  fetchOptions.headers['Content-Type'] = 'application/json';

  let request = null;
  if (typeof(this.id) !== 'undefined') {
    request = fetch('/rules/' + this.id, fetchOptions);
  } else {
    fetchOptions.method = 'POST';
    request = fetch('/rules/', fetchOptions).then(res => {
      return res.json();
    }).then(rule => {
      this.id = rule.id;
    });
  }
  return request;
};

/**
 * Delete the rule
 * @return {Promise}
 */
Rule.prototype.delete = function() {
  let fetchOptions = {
    headers: API.headers(),
    method: 'DELETE'
  };

  if (typeof(this.id) === 'undefined') {
    return;
  }

  return fetch('/rules/' + this.id, fetchOptions);
};

/**
 * Convert this rule into a serialized description
 * @return {RuleDescription?} description or null if not a valid rule
 */
Rule.prototype.toDescription = function() {
  if (!this.trigger || !this.effect) {
    return null;
  }
  return {
    enabled: this.enabled,
    name: this.name,
    trigger: this.trigger,
    effect: this.effect
  };
};

// Helper function for selecting the thing corresponding to a property
const RuleUtils = {
  byProperty: function byProperty(property) {
    return function(option) {
      let optProp = option.properties[property.name];
      return optProp && (optProp.href === property.href);
    };
  }
};

/**
 * Convert the rule's trigger's description to a human-readable string
 * @return {String}
 */
Rule.prototype.toTriggerHumanDescription = function() {
  let triggerThing = this.gateway.things.filter(
    RuleUtils.byProperty(this.trigger.property)
  )[0];

  let triggerStr = `${triggerThing.name} `;
  if (this.trigger.type === 'BooleanTrigger') {
    triggerStr += 'is ';
    if (!this.trigger.onValue) {
      triggerStr += 'not ';
    }
    triggerStr += this.trigger.property.name;
  } else {
    triggerStr += `${this.trigger.property.name} is `;
    if (this.trigger.levelType === 'LESS') {
      triggerStr += 'less than ';
    } else {
      triggerStr += 'greater than ';
    }
    triggerStr += this.trigger.level;
  }

  return triggerStr;
};

/**
 * Convert the rule's effect's description to a human-readable string
 * @return {String}
 */
Rule.prototype.toEffectHumanDescription = function() {
  let effectThing = this.gateway.things.filter(
    RuleUtils.byProperty(this.effect.property)
  )[0];

  let effectStr = '';
  if (this.effect.property.name === 'on' &&
    effectThing.type === 'onOffSwitch') {
    effectStr = `turn ${effectThing.name} `;
    if (this.effect.value) {
      effectStr += 'on';
    } else {
      effectStr += 'off';
    }
    if (this.effect.type === 'SET') {
      effectStr += ' permanently';
    }
    return effectStr;
  }
  if (this.effect.type === 'SET') {
    effectStr += 'set ';
  } else {
    effectStr += 'pulse ';
  }

  effectStr += `${effectThing.name} ${this.effect.property.name} to `;
  effectStr += this.effect.value;

  return effectStr;
};

/**
 * Convert the rule's description to a human-readable string
 * @return {String}
 */
Rule.prototype.toHumanDescription = function() {
  let triggerStr = '???';
  let effectStr = '???';

  if (this.trigger) {
    triggerStr = this.toTriggerHumanDescription();
  }
  if (this.effect) {
    effectStr = this.toEffectHumanDescription();
  }
  return 'If ' + triggerStr + ' then ' + effectStr;
};

/**
 * Set the trigger of the Rule, updating the server model if valid
 * @return {Promise}
 */
Rule.prototype.setTrigger = function(trigger) {
  this.trigger = trigger;
  return this.update();
};

/**
 * Set the effect of the Rule, updating the server model if valid
 * @return {Promise}
 */
Rule.prototype.setEffect = function(effect) {
  this.effect = effect;
  return this.update();
};

<commit_msg>Remove onOffSwitch check from rule summarizing<commit_after>/**
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.*
 */

/* global API */

/**
 * Model of a Rule loaded from the Rules Engine
 * @constructor
 * @param {Gateway} gateway - The remote gateway to which to talk
 * @param {RuleDescription?} desc - Description of the rule to load
 * @param {Function?} onUpdate - Listener for when update is called
 */
function Rule(gateway, desc, onUpdate) {
  this.gateway = gateway;
  this.onUpdate = onUpdate;

  if (desc) {
    this.id = desc.id;
    this.enabled = desc.enabled;
    if (desc.name) {
      this.name = desc.name;
    } else {
      this.name = 'Rule Name';
    }
    this.trigger = desc.trigger;
    this.effect = desc.effect;
  } else {
    this.enabled = true;
  }
}

/**
 * Validate and save the rule
 * @return {Promise}
 */
Rule.prototype.update = function() {
  if (this.onUpdate) {
    this.onUpdate();
  }
  let desc = this.toDescription();
  if (!desc) {
    return Promise.reject('invalid description');
  }

  let fetchOptions = {
    headers: API.headers(),
    method: 'PUT',
    body: JSON.stringify(desc)
  };
  fetchOptions.headers['Content-Type'] = 'application/json';

  let request = null;
  if (typeof(this.id) !== 'undefined') {
    request = fetch('/rules/' + this.id, fetchOptions);
  } else {
    fetchOptions.method = 'POST';
    request = fetch('/rules/', fetchOptions).then(res => {
      return res.json();
    }).then(rule => {
      this.id = rule.id;
    });
  }
  return request;
};

/**
 * Delete the rule
 * @return {Promise}
 */
Rule.prototype.delete = function() {
  let fetchOptions = {
    headers: API.headers(),
    method: 'DELETE'
  };

  if (typeof(this.id) === 'undefined') {
    return;
  }

  return fetch('/rules/' + this.id, fetchOptions);
};

/**
 * Convert this rule into a serialized description
 * @return {RuleDescription?} description or null if not a valid rule
 */
Rule.prototype.toDescription = function() {
  if (!this.trigger || !this.effect) {
    return null;
  }
  return {
    enabled: this.enabled,
    name: this.name,
    trigger: this.trigger,
    effect: this.effect
  };
};

// Helper function for selecting the thing corresponding to a property
const RuleUtils = {
  byProperty: function byProperty(property) {
    return function(option) {
      let optProp = option.properties[property.name];
      return optProp && (optProp.href === property.href);
    };
  }
};

/**
 * Convert the rule's trigger's description to a human-readable string
 * @return {String}
 */
Rule.prototype.toTriggerHumanDescription = function() {
  let triggerThing = this.gateway.things.filter(
    RuleUtils.byProperty(this.trigger.property)
  )[0];

  let triggerStr = `${triggerThing.name} `;
  if (this.trigger.type === 'BooleanTrigger') {
    triggerStr += 'is ';
    if (!this.trigger.onValue) {
      triggerStr += 'not ';
    }
    triggerStr += this.trigger.property.name;
  } else {
    triggerStr += `${this.trigger.property.name} is `;
    if (this.trigger.levelType === 'LESS') {
      triggerStr += 'less than ';
    } else {
      triggerStr += 'greater than ';
    }
    triggerStr += this.trigger.level;
  }

  return triggerStr;
};

/**
 * Convert the rule's effect's description to a human-readable string
 * @return {String}
 */
Rule.prototype.toEffectHumanDescription = function() {
  let effectThing = this.gateway.things.filter(
    RuleUtils.byProperty(this.effect.property)
  )[0];

  let effectStr = '';
  if (this.effect.property.name === 'on') {
    effectStr = `turn ${effectThing.name} `;
    if (this.effect.value) {
      effectStr += 'on';
    } else {
      effectStr += 'off';
    }
    if (this.effect.type === 'SET') {
      effectStr += ' permanently';
    }
    return effectStr;
  }
  if (this.effect.type === 'SET') {
    effectStr += 'set ';
  } else {
    effectStr += 'pulse ';
  }

  effectStr += `${effectThing.name} ${this.effect.property.name} to `;
  effectStr += this.effect.value;

  return effectStr;
};

/**
 * Convert the rule's description to a human-readable string
 * @return {String}
 */
Rule.prototype.toHumanDescription = function() {
  let triggerStr = '???';
  let effectStr = '???';

  if (this.trigger) {
    triggerStr = this.toTriggerHumanDescription();
  }
  if (this.effect) {
    effectStr = this.toEffectHumanDescription();
  }
  return 'If ' + triggerStr + ' then ' + effectStr;
};

/**
 * Set the trigger of the Rule, updating the server model if valid
 * @return {Promise}
 */
Rule.prototype.setTrigger = function(trigger) {
  this.trigger = trigger;
  return this.update();
};

/**
 * Set the effect of the Rule, updating the server model if valid
 * @return {Promise}
 */
Rule.prototype.setEffect = function(effect) {
  this.effect = effect;
  return this.update();
};

<|endoftext|>


<commit_before>'use strict';

/* jshint globalstrict: true */
/* global dc,d3,crossfilter,colorbrewer */

// All of our charts are now accessible globally.
var votePartyChart = dc.rowChart("#party-chart");
var mapChart = dc.geoChoroplethChart("#map-chart");
var nominateScatterChart = dc.scatterPlot("#scatter-chart");
var globalPartyDimension = null;
var globalData;

// Makes the bootstrap tooltip run for votes from before states were contiguous.
$(document).ready(function(){$('[data-toggle="tooltip"]').tooltip();});

// Initial asynchronous load
(function loadData(){
    if (chamber == "House") {
        queue()
          .defer(d3.json, "/api/download/"+rcID)
          .defer(d3.json, "/static/json/districts"+congressNum+".json")
          .await(drawWidgets);
    } else if (chamber == "Senate") {
        queue()
          .defer(d3.json, "/api/download/"+rcID)
          .defer(d3.json, "/static/json/states"+congressNum+".json")
          .await(drawWidgets);
    }
})();

var eW = 0; var eH = 0;
function tooltipText(d)
{
	var nays=0; var yeas=0; var abs=0;
	if(chamber=="House") { var result = "
" + d.key + "
"; }
	else { var result = "
" + stateMap[d.key] + "
"; }
	for(var i=0; i<d.value.members.length; i++)
	{
		var colorVote = partyColors[d.value.members[i].vote + partyNameSimplify(d.value.members[i].party)];
		// Tooltip data display:
		if(i<5) { result += "
" + d.value.members[i].name + " ("+partyNameSimplify(d.value.members[i].party).substr(0,1)+") - "+d.value.members[i].vote+"
"; }
		else
		{
			if(d.value.members[i].vote=="Nay") { nays=nays+1; }
			else if(d.value.members[i].vote=="Yea") { yeas=yeas+1; }
			else { abs=abs+1; }
		}
	}
	if(i>=5)
	{
		result+= "
+";
		if(yeas) { result += yeas+" other Yea"+(yeas!=1?"s":""); }
		if(nays)
		{
			if(yeas) { result += ", "; }
			result += nays+" other Nay"+(nays!=1?"s":"");
		}
		if(abs)
		{
			if(yeas || nays) { result += ", "; }
			result += abs+" other Abs";
		}
	}
	return(result);
}

// If there's an error loading the map, still load the vote data, and just fail as gracefully as possible.
function drawWidgetsFailMap(error, data)
{
	drawWidgets(error, data, undefined);
}

var failedMapLoad=0, fallback=0;
function drawWidgets(error, data, geodata)
{
	// If we have an error loading the map data, try a fallback map.
	if(fallback==0 && geodata==undefined && error.status==404 && error.responseURL.indexOf(".json")!=-1)
	{
		var tryLoadingOneLower = "/static/"+error.responseURL.replace(congressNum,congressNum-1).split("/static/")[1];
		fallback=1;
		queue().defer(d3.json, "/api/download/"+rcID).defer(d3.json, tryLoadingOneLower).await(drawWidgets);
		return(0);
	}
	// If we still have an error, give up on the map but still load the vote.
	else if(failedMapLoad==0 && (data==undefined || geodata==undefined))
	{
		var errorMessage = "Unknown error loading vote data.";
		if(error.status==404 && error.responseURL.indexOf(".json")!=-1)
		{
			errorMessage = "Unable to download geographic data for this session.";
			queue().defer(d3.json, "/api/download/"+rcID).await(drawWidgetsFailMap);
		}
		$("#errorContent > div > div.errorMessage").html(errorMessage);
		$("#errorContent").animate({"height": "toggle", "opacity": "toggle"},"slow");
		$("#geoMap").hide();
		$("#map-chart").attr("id","junk");
		failedMapLoad = 1;
		dc.chartRegistry.deregister(dc.chartRegistry.list()[1]);
		return(0);
	}

	//$("#vote_chart_float").stick_in_parent({offset_top: 20});

	$("#loadBar").slideToggle();
	globalData = data;
	$("#loadedContent").animate({"height": "toggle", "opacity": "toggle"},"slow");

	var ndx = crossfilter(data.rollcalls[0].votes);
	var all = ndx.groupAll();

	// Dimension 1: What type of vote you cast
	var voteDimension = ndx.dimension(function(d) { return d.vote; });
	var voteGroup = voteDimension.group(); // Grouping is exact

	// Dimension 2: What party you are in
	var partyDimension = ndx.dimension(function(d) { return partyNameSimplify(d.party_short_name); });
	var partyGroup = partyDimension.group(); // Grouping is exact
	globalPartyDimension = partyDimension;

	// Dimension 3: What type of vote you cast and what party you are in.
	var votePartyDimension = ndx.dimension(function(d) { return d.vote + partyNameSimplify(d.party_short_name); });
	var votePartyGroup = votePartyDimension.group(); // Grouping is exact

	// Dimension 4: Coordinates of vote
	var xDimension = ndx.dimension(
		//Project outlying ideal points onto the outer circle
		function(d) {
			var x = d.x;  var y = d.y;
		        var dlen = Math.sqrt(x*x + y*y);
		        if (dlen>1.0) {
			    x = x/dlen;
			    y = y/dlen;
			}
		        // JBL: Hack to stop new members from being placed in the upper left corner of the scatter.
		        if (typeof d.x == 'undefined') {
			    x = -99; y = -99;
			}
			return [x, y];
		}
	);

	var xGroup = xDimension.group().reduce(
		function (p, d)
		{
			p.members.push(d);
			return p;
		},

		function (p, d)
		{
			var index = p.members.indexOf(d);
			if (index > -1)
			{
				p.members.splice(index, 1);
			}
			return p;
		},

		function ()
		{
			return {members: []} ;
		}); // This is not super clear to me.

	// Dimension 5: What state you're from.
	var stateDimension = ndx.dimension(function(d) { return d.state_abbrev; });
	var stateGroup = stateDimension.group().reduce(
		function (p, d)
		{
			p.members.push(d);
			return p;
		},

		function (p, d)
		{
			var index = p.members.indexOf(d);
			if (index > -1) {
				p.members.splice(index, 1);
			}
			return p;
		},

		function ()
		{
			return {members: []} ;
		});

	// Dimension 6: Which district you are from
	var districtDimension = ndx.dimension(function(d) {
		return d.district;
	});

	var districtGroup = districtDimension.group().reduce(
		function (p, d)
		{
			// Add at large members
			var atlargecodes = [d.state + "00", d.state+"98", d.state+"99"];
			var atlarge = $.grep(data.rollcalls[0].votes, function(e)
			{
				return e.district==atlargecodes[0] || e.district==atlargecodes[1] || e.district==atlargecodes[2];
			});
			$.each(atlarge, function(member) {
				p.members.push(atlarge[member]);
			});
			p.members.push(d);
			return p;
		},

	function (p, d) {
		//console.log('huh');
            // Remove at large members
            var atlargecode = d.state + "00";
            var atlarge = $.grep(data.rollcalls[0].votes, function(e){return e.district == atlargecode; });
            $.each(atlarge, function(member) {
                p.members.splice( $.inArray(atlarge[member], p.members), 1);
            });

            var index = p.members.indexOf(d);
            if (index > -1) {
              p.members.splice(index, 1);
            }
            return p;
        },

        function () {
            return {members: []} ;
        });

	// DIMENSIONS HAVE BEEN DEFINED =========

	// NOW BEGIN CHART SPECIFICATIONS =======
	votePartyChart
		.width(280).height(320)
		.dimension(votePartyDimension).group(votePartyGroup)
		.elasticX(true)
		.colorCalculator(function (d) {
			return partyColors[d.key];
		})
		.fixedBarHeight(24).gap(10)
		.labelOffsetX(40)
		.label(function(d)
		{
			if(d.key.substr(0,3)=="Abs") { var textLabel = d.key.substr(3,d.key.length)+": Not Voting"; }
			else { var textLabel = d.key.substr(3,d.key.length)+": "+d.key.substr(0,3) }
			return textLabel
		})
		.ordering(function(d){ // Sort Yea-to-Nay, Alphabetically, set independents separately.
			var score = 0
			switch(d.key.substr(0,3))
			{
				case "Yea": score = 9; break;
				case "Nay": score = 6; break;
				case "Abs": score = 3; break
			}
			switch(d.key.substr(3,d.key.length))
			{
				case "Democrat": score=score+2; break;
				case "Republican": score=score+1; break;
				default: score=score+0; break;
			}
			return -score;
		})
		.transitionDuration(200)
        	.xAxis().ticks(4);

	// Nominate scatter chart setup
	nominateScatterChart
		.width(890)
		.height(424)
		.margins({top:25,right:25,bottom:75,left:75})
		.dimension(xDimension)
		.mouseZoomable(false)
		.group(xGroup)
		.symbolSize(7)
		.colorCalculator(function (d) {
			var color = "#CCC";
			try {
				if(d.value.members.length > 0){
				color = blendColors(d.value.members);
				}
			}catch(e){
			}
			return color;
		})
		.highlightedSize(10)
		.x(d3.scale.linear().domain([-1.0, 1.0]))
		.y(d3.scale.linear().domain([-1.2, 1.2]));

	// Updates the total number of units selected on the selection bar.
	dc.dataCount("#data-count")
		.dimension(ndx)
		.group(all);

	// Setting up the map chart only if we load geo data.
	if(!failedMapLoad)
	{
		// Add the tooltip to the body and hide it.
		var baseToolTip = d3.select("body").append("div").attr("class", "d3-tip").attr("id","mapTooltip").style("visibility","hidden");
		// Set up topographic data
		var mapTopo = topojson.feature(geodata, (chamber=="House")?geodata.objects.districts:geodata.objects.states).features;
		// Define the chart
		mapChart
			.width(890).height(500) // Basic dimensions
			.dimension((chamber=="House")?districtDimension:stateDimension) // How the data are separated and grouped.
			.group((chamber=="House")?districtGroup:stateGroup)
			.colorCalculator(function (d) { // What color does each unit use
				//if(d!=undefined && d.members.length>0) { for(var i=0;i!=d.members.length;i++) { d.members[i].party="Democrat"; } }
				var color = "#eee";
				try {
					if(d.members.length > 0){ // If there are any members here, blend their colours.
						color = blendColors(d.members);
					}
				}catch(e){
					//console.log("MC: " + e);
				}
				return color;
			})
			.overlayGeoJson(mapTopo, (chamber=="House")?"district":"state", function (d) { // Folds in the data.
				return d.id;
			})
			.renderTitle(false) // No default tooltips if you mouse over the map.
			.on("postRender", function(c){ // Attach the tooltip code.
				c.svg() // Chart SVG
					.selectAll("path") // Attach the listeners to every path (district) item in the SVG
					.on('mouseover', function(d,i) // When you mouseover, it's a new district, set up the tooltip and make it visible
					{
						var districtSet = c.data();
						var result = $.grep(c.data(), function(e){
							return e.key == d.id;
						});
						if(result[0]==undefined) { baseToolTip.html("
"+d.id+"
 This district was vacant at the time of the vote."); } // Don't tooltip null results.
						else { baseToolTip.html(tooltipText(result[0])); }
						eH = baseToolTip.style("height"); // We need these for centering the tooltip appropriately.
						eW = baseToolTip.style("width");
						baseToolTip.style("visibility","visible");
					})
					.on('mouseout', function() { baseToolTip.style("visibility","hidden"); }) // If you mouse out of the districts, hide the tooltip
					.on('mousemove', function(d, i){ // If you move your mouse within the district, update the position of the tooltip.
						baseToolTip.style("top",(event.pageY+32)+"px").style("left",(event.pageX-(parseInt(eW.substr(0,eW.length-2))/2))+"px");
					});
			});
	}

	// We are done defining everything, now let's just run our ancillary functions.
	dc.renderAll();
	decorateNominate(nominateScatterChart,data);
	if(!failedMapLoad) mapChart.on("filtered", pollFilters);
	votePartyChart.on("filtered", pollFilters);
	nominateScatterChart.on("filtered", pollFilters);
	outVotes();

}

// Easier to update steps to take on a full filter reset by running this.
function doFullFilterReset()
{
	// Hide the bar.
	$("#selectionFilterBar").slideUp();
	// Deselect everything.
	dc.filterAll();
	// Draw the charts from scratch.
	dc.redrawAll();
	// Re-apply our decoration hack.
	decorateNominate(nominateScatterChart, globalData);
	//updateVoteChart();
}
<commit_msg>Added an optimization to tell the scatterplot that points with undefined nominate actually don't exist, rather than just plotting them outside the canvas.<commit_after>'use strict';

/* jshint globalstrict: true */
/* global dc,d3,crossfilter,colorbrewer */

// All of our charts are now accessible globally.
var votePartyChart = dc.rowChart("#party-chart");
var mapChart = dc.geoChoroplethChart("#map-chart");
var nominateScatterChart = dc.scatterPlot("#scatter-chart");
var globalPartyDimension = null;
var globalData;

// Makes the bootstrap tooltip run for votes from before states were contiguous.
$(document).ready(function(){$('[data-toggle="tooltip"]').tooltip();});

// Initial asynchronous load
(function loadData(){
    if (chamber == "House") {
        queue()
          .defer(d3.json, "/api/download/"+rcID)
          .defer(d3.json, "/static/json/districts"+congressNum+".json")
          .await(drawWidgets);
    } else if (chamber == "Senate") {
        queue()
          .defer(d3.json, "/api/download/"+rcID)
          .defer(d3.json, "/static/json/states"+congressNum+".json")
          .await(drawWidgets);
    }
})();

var eW = 0; var eH = 0;
function tooltipText(d)
{
	var nays=0; var yeas=0; var abs=0;
	if(chamber=="House") { var result = "
" + d.key + "
"; }
	else { var result = "
" + stateMap[d.key] + "
"; }
	for(var i=0; i<d.value.members.length; i++)
	{
		var colorVote = partyColors[d.value.members[i].vote + partyNameSimplify(d.value.members[i].party)];
		// Tooltip data display:
		if(i<5) { result += "
" + d.value.members[i].name + " ("+partyNameSimplify(d.value.members[i].party).substr(0,1)+") - "+d.value.members[i].vote+"
"; }
		else
		{
			if(d.value.members[i].vote=="Nay") { nays=nays+1; }
			else if(d.value.members[i].vote=="Yea") { yeas=yeas+1; }
			else { abs=abs+1; }
		}
	}
	if(i>=5)
	{
		result+= "
+";
		if(yeas) { result += yeas+" other Yea"+(yeas!=1?"s":""); }
		if(nays)
		{
			if(yeas) { result += ", "; }
			result += nays+" other Nay"+(nays!=1?"s":"");
		}
		if(abs)
		{
			if(yeas || nays) { result += ", "; }
			result += abs+" other Abs";
		}
	}
	return(result);
}

// If there's an error loading the map, still load the vote data, and just fail as gracefully as possible.
function drawWidgetsFailMap(error, data)
{
	drawWidgets(error, data, undefined);
}

var failedMapLoad=0, fallback=0;
function drawWidgets(error, data, geodata)
{
	// If we have an error loading the map data, try a fallback map.
	if(fallback==0 && geodata==undefined && error.status==404 && error.responseURL.indexOf(".json")!=-1)
	{
		var tryLoadingOneLower = "/static/"+error.responseURL.replace(congressNum,congressNum-1).split("/static/")[1];
		fallback=1;
		queue().defer(d3.json, "/api/download/"+rcID).defer(d3.json, tryLoadingOneLower).await(drawWidgets);
		return(0);
	}
	// If we still have an error, give up on the map but still load the vote.
	else if(failedMapLoad==0 && (data==undefined || geodata==undefined))
	{
		var errorMessage = "Unknown error loading vote data.";
		if(error.status==404 && error.responseURL.indexOf(".json")!=-1)
		{
			errorMessage = "Unable to download geographic data for this session.";
			queue().defer(d3.json, "/api/download/"+rcID).await(drawWidgetsFailMap);
		}
		$("#errorContent > div > div.errorMessage").html(errorMessage);
		$("#errorContent").animate({"height": "toggle", "opacity": "toggle"},"slow");
		$("#geoMap").hide();
		$("#map-chart").attr("id","junk");
		failedMapLoad = 1;
		dc.chartRegistry.deregister(dc.chartRegistry.list()[1]);
		return(0);
	}

	//$("#vote_chart_float").stick_in_parent({offset_top: 20});

	$("#loadBar").slideToggle();
	globalData = data;
	$("#loadedContent").animate({"height": "toggle", "opacity": "toggle"},"slow");

	var ndx = crossfilter(data.rollcalls[0].votes);
	var all = ndx.groupAll();

	// Dimension 1: What type of vote you cast
	var voteDimension = ndx.dimension(function(d) { return d.vote; });
	var voteGroup = voteDimension.group(); // Grouping is exact

	// Dimension 2: What party you are in
	var partyDimension = ndx.dimension(function(d) { return partyNameSimplify(d.party_short_name); });
	var partyGroup = partyDimension.group(); // Grouping is exact
	globalPartyDimension = partyDimension;

	// Dimension 3: What type of vote you cast and what party you are in.
	var votePartyDimension = ndx.dimension(function(d) { return d.vote + partyNameSimplify(d.party_short_name); });
	var votePartyGroup = votePartyDimension.group(); // Grouping is exact

	// Dimension 4: Coordinates of vote
	var xDimension = ndx.dimension(
		//Project outlying ideal points onto the outer circle
		function(d) {
			var x = d.x;  var y = d.y;
		        var dlen = Math.sqrt(x*x + y*y);
		        if (dlen>1.0) {
			    x = x/dlen;
			    y = y/dlen;
			}
		        // JBL: Hack to stop new members from being placed in the upper left corner of the scatter.
		        if (typeof d.x == 'undefined') {
			    x = -99; y = -99;
			}
			return [x, y];
		}
	);

	var xGroup = xDimension.group().reduce(
		function (p, d)
		{
			p.members.push(d);
			return p;
		},

		function (p, d)
		{
			var index = p.members.indexOf(d);
			if (index > -1)
			{
				p.members.splice(index, 1);
			}
			return p;
		},

		function ()
		{
			return {members: []} ;
		}); // This is not super clear to me.

	// Dimension 5: What state you're from.
	var stateDimension = ndx.dimension(function(d) { return d.state_abbrev; });
	var stateGroup = stateDimension.group().reduce(
		function (p, d)
		{
			p.members.push(d);
			return p;
		},

		function (p, d)
		{
			var index = p.members.indexOf(d);
			if (index > -1) {
				p.members.splice(index, 1);
			}
			return p;
		},

		function ()
		{
			return {members: []} ;
		});

	// Dimension 6: Which district you are from
	var districtDimension = ndx.dimension(function(d) {
		return d.district;
	});

	var districtGroup = districtDimension.group().reduce(
		function (p, d)
		{
			// Add at large members
			var atlargecodes = [d.state + "00", d.state+"98", d.state+"99"];
			var atlarge = $.grep(data.rollcalls[0].votes, function(e)
			{
				return e.district==atlargecodes[0] || e.district==atlargecodes[1] || e.district==atlargecodes[2];
			});
			$.each(atlarge, function(member) {
				p.members.push(atlarge[member]);
			});
			p.members.push(d);
			return p;
		},

	function (p, d) {
		//console.log('huh');
            // Remove at large members
            var atlargecode = d.state + "00";
            var atlarge = $.grep(data.rollcalls[0].votes, function(e){return e.district == atlargecode; });
            $.each(atlarge, function(member) {
                p.members.splice( $.inArray(atlarge[member], p.members), 1);
            });

            var index = p.members.indexOf(d);
            if (index > -1) {
              p.members.splice(index, 1);
            }
            return p;
        },

        function () {
            return {members: []} ;
        });

	// DIMENSIONS HAVE BEEN DEFINED =========

	// NOW BEGIN CHART SPECIFICATIONS =======
	votePartyChart
		.width(280).height(320)
		.dimension(votePartyDimension).group(votePartyGroup)
		.elasticX(true)
		.colorCalculator(function (d) {
			return partyColors[d.key];
		})
		.fixedBarHeight(24).gap(10)
		.labelOffsetX(40)
		.label(function(d)
		{
			if(d.key.substr(0,3)=="Abs") { var textLabel = d.key.substr(3,d.key.length)+": Not Voting"; }
			else { var textLabel = d.key.substr(3,d.key.length)+": "+d.key.substr(0,3) }
			return textLabel
		})
		.ordering(function(d){ // Sort Yea-to-Nay, Alphabetically, set independents separately.
			var score = 0
			switch(d.key.substr(0,3))
			{
				case "Yea": score = 9; break;
				case "Nay": score = 6; break;
				case "Abs": score = 3; break
			}
			switch(d.key.substr(3,d.key.length))
			{
				case "Democrat": score=score+2; break;
				case "Republican": score=score+1; break;
				default: score=score+0; break;
			}
			return -score;
		})
		.transitionDuration(200)
        	.xAxis().ticks(4);

	// Nominate scatter chart setup
	nominateScatterChart
		.width(890)
		.height(424)
		.margins({top:25,right:25,bottom:75,left:75})
		.dimension(xDimension)
		.mouseZoomable(false)
		.group(xGroup)
		.symbolSize(7)
		.colorCalculator(function (d) {
			var color = "#CCC";
			try {
				if(d.value.members.length > 0){
				color = blendColors(d.value.members);
				}
			}catch(e){
			}
			return color;
		})
		.highlightedSize(10)
		.existenceAccessor(function(d) {
			return parseFloat(String(d.key).split(",")[0])>-98; })
		.x(d3.scale.linear().domain([-1.0, 1.0]))
		.y(d3.scale.linear().domain([-1.2, 1.2]));

	// Updates the total number of units selected on the selection bar.
	dc.dataCount("#data-count")
		.dimension(ndx)
		.group(all);

	// Setting up the map chart only if we load geo data.
	if(!failedMapLoad)
	{
		// Add the tooltip to the body and hide it.
		var baseToolTip = d3.select("body").append("div").attr("class", "d3-tip").attr("id","mapTooltip").style("visibility","hidden");
		// Set up topographic data
		var mapTopo = topojson.feature(geodata, (chamber=="House")?geodata.objects.districts:geodata.objects.states).features;
		// Define the chart
		mapChart
			.width(890).height(500) // Basic dimensions
			.dimension((chamber=="House")?districtDimension:stateDimension) // How the data are separated and grouped.
			.group((chamber=="House")?districtGroup:stateGroup)
			.colorCalculator(function (d) { // What color does each unit use
				//if(d!=undefined && d.members.length>0) { for(var i=0;i!=d.members.length;i++) { d.members[i].party="Democrat"; } }
				var color = "#eee";
				try {
					if(d.members.length > 0){ // If there are any members here, blend their colours.
						color = blendColors(d.members);
					}
				}catch(e){
					//console.log("MC: " + e);
				}
				return color;
			})
			.overlayGeoJson(mapTopo, (chamber=="House")?"district":"state", function (d) { // Folds in the data.
				return d.id;
			})
			.renderTitle(false) // No default tooltips if you mouse over the map.
			.on("postRender", function(c){ // Attach the tooltip code.
				c.svg() // Chart SVG
					.selectAll("path") // Attach the listeners to every path (district) item in the SVG
					.on('mouseover', function(d,i) // When you mouseover, it's a new district, set up the tooltip and make it visible
					{
						var districtSet = c.data();
						var result = $.grep(c.data(), function(e){
							return e.key == d.id;
						});
						if(result[0]==undefined) { baseToolTip.html("
"+d.id+"
 This district was vacant at the time of the vote."); } // Don't tooltip null results.
						else { baseToolTip.html(tooltipText(result[0])); }
						eH = baseToolTip.style("height"); // We need these for centering the tooltip appropriately.
						eW = baseToolTip.style("width");
						baseToolTip.style("visibility","visible");
					})
					.on('mouseout', function() { baseToolTip.style("visibility","hidden"); }) // If you mouse out of the districts, hide the tooltip
					.on('mousemove', function(d, i){ // If you move your mouse within the district, update the position of the tooltip.
						baseToolTip.style("top",(event.pageY+32)+"px").style("left",(event.pageX-(parseInt(eW.substr(0,eW.length-2))/2))+"px");
					});
			});
	}

	// We are done defining everything, now let's just run our ancillary functions.
	dc.renderAll();
	decorateNominate(nominateScatterChart,data);
	if(!failedMapLoad) mapChart.on("filtered", pollFilters);
	votePartyChart.on("filtered", pollFilters);
	nominateScatterChart.on("filtered", pollFilters);
	outVotes();

}

// Easier to update steps to take on a full filter reset by running this.
function doFullFilterReset()
{
	// Hide the bar.
	$("#selectionFilterBar").slideUp();
	// Deselect everything.
	dc.filterAll();
	// Draw the charts from scratch.
	dc.redrawAll();
	// Re-apply our decoration hack.
	decorateNominate(nominateScatterChart, globalData);
	//updateVoteChart();
}
<|endoftext|>


<commit_before>$(document).ready(function(){
  $('.slideshow').cycle({
    fx: 'fade',
    timeout: 7000,
    pager: "#pager"
  });
  $('a').click(function(){ $(this).blur(); });

  var current = $.url.segment(0);
  console.log(current);
  if (current) {
    $('.nav li.'+current).addClass('active');
  }else{
    $('.nav .home').addClass('active');
  }
});

var gaJsHost = (("https:" == document.location.protocol) ? "https://ssl." : "http://www.");
document.write(unescape("%3Cscript src='" + gaJsHost + "google-analytics.com/ga.js' type='text/javascript'%3E%3C/script%3E"));

try {
    var pageTracker = _gat._getTracker("UA-15984160-1");
    pageTracker._trackPageview();
} catch(err) {}
<commit_msg>Fixed "Firefox #nav highlighting" issue reported on github.<commit_after>$(document).ready(function(){
  $('.slideshow').cycle({
    fx: 'fade',
    timeout: 7000,
    pager: "#pager"
  });
  $('a').click(function(){ $(this).blur(); });

  if ($.url.segment(0)) {
    $('.nav li.'+$.url.segment(0)).addClass('active');
  } else {
    $('.nav .home').addClass('active');
  }
});

var gaJsHost = (("https:" == document.location.protocol) ? "https://ssl." : "http://www.");
document.write(unescape("%3Cscript src='" + gaJsHost + "google-analytics.com/ga.js' type='text/javascript'%3E%3C/script%3E"));

try {
    var pageTracker = _gat._getTracker("UA-15984160-1");
    pageTracker._trackPageview();
} catch(err) {}
<|endoftext|>


<commit_before>/* TeamEditPage */

(function(module, sstt) {

    module.ModelView = Backbone.View.extend({

        template: JST['app/scripts/TeamEditPage/TeamEditPageTpl.ejs'],

        events: {
            "click #watchers": "showWatchers",
            "click #developers": "showDevelopers",
            "click #techleads": "showTeachLeads",
            "click #ok_btn": "hideConfirm"
        },

        subscriptions: {
            "TeamPage:TeamSelected": "render",
            "ContextMenu:Back": "removeTeamPage",
            "ContextMenu:BackFromTeamEditPage": "removeTeamPage"
        },

        render: function(team_id) {
            this.$el.append(this.template());
            mediator.pub("TeamEditPage:Open", { element: this.$el,
                                                team_id: team_id
                                                });
            this.showWatchers();

            return this;
        },

        showWatchers: function() {
            mediator.pub("TeamEditPage:RoleSetUp", "watcher");
        },

        showDevelopers: function() {
            mediator.pub("TeamEditPage:RoleSetUp", "developer");
        },

        showTeachLeads: function() {
            mediator.pub("TeamEditPage:RoleSetUp", "techlead");
        },

        hideConfirm: function() {
            this.$el.find("#save_confirm").addClass("hidden");
        },

        removeTeamPage: function() {
            this.$el.removeClass("hiddenTeams");
            this.$el.find(".team-edit-page").remove();
        }

    });

})(app.TeamEditPage, sstt);
<commit_msg>Update TeamEditPageModelView.js<commit_after>/* TeamEditPage */

(function(module, sstt) {

    module.ModelView = Backbone.View.extend({

        template: JST['app/scripts/TeamEditPage/TeamEditPageTpl.ejs'],

        events: {
            "click #watchers": "showWatchers",
            "click #developers": "showDevelopers",
            "click #techleads": "showTeachLeads",
            "click #ok_btn": "hideConfirm"
        },

        subscriptions: {
            "TeamPage:TeamSelected": "render",
            "ContextMenu:Back": "removeTeamPage",
            "ContextMenu:BackFromTeamEditPage": "removeTeamPage"
        },

        render: function(team_id) {
            this.$el.append(this.template());
            mediator.pub("TeamEditPage:Open", { element: this.$el,
                                                team_id: team_id
                                                });
            this.showDevelopers();

            return this;
        },

        showWatchers: function() {
            mediator.pub("TeamEditPage:RoleSetUp", "watcher");
            $("#watchers").css("background-color", "white");
            $("#developers").css("background-color", "none");
            $("#techleads").css("background-color", "none");
        },

        showDevelopers: function() {
            mediator.pub("TeamEditPage:RoleSetUp", "developer");
            $("#watchers").css("background-color", "none");
            $("#developers").css("background-color", "white");
            $("#techleads").css("background-color", "none");
        },

        showTeachLeads: function() {
            mediator.pub("TeamEditPage:RoleSetUp", "techlead");
            $("#watchers").css("background-color", "none");
            $("#developers").css("background-color", "none");
            $("#techleads").css("background-color", "white");
        },

        hideConfirm: function() {
            this.$el.find("#save_confirm").addClass("hidden");
        },

        removeTeamPage: function() {
            this.$el.removeClass("hiddenTeams");
            this.$el.find(".team-edit-page").remove();
        }

    });

})(app.TeamEditPage, sstt);
<|endoftext|>