<commit_before>/*
 * Copyright (C) 2016 - present Instructure, Inc.
 *
 * This file is part of Canvas.
 *
 * Canvas is free software: you can redistribute it and/or modify it under
 * the terms of the GNU Affero General Public License as published by the Free
 * Software Foundation, version 3 of the License.
 *
 * Canvas 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/>.
 */

import _ from 'underscore'
import Big from 'big.js'
import round from 'compiled/util/round'
import AssignmentGroupGradeCalculator from '../gradebook/AssignmentGroupGradeCalculator'

function addWithPrecision (a, b) {
  if (a == null && b == null) {
    return new Big(0)
  } else if (a == null) {
    return new Big(b)
  } else if (b == null) {
    return new Big(a)
  } else {
    return new Big(a).plus(b)
  }
}

function sum (collection) {
  const bigValue = _.reduce(collection, addWithPrecision, 0)
  return Number.parseFloat(bigValue, 10)
}

function sumBy (collection, attr) {
  const values = _.map(collection, attr);
  return sum(values);
}

function getWeightedPercent ({ score, possible, weight }) {
  return score ? (score / possible) * weight : 0;
}

function combineAssignmentGroupGrades (assignmentGroupGrades, includeUngraded, options) {
  const scopedAssignmentGroupGrades = _.map(assignmentGroupGrades, (assignmentGroupGrade) => {
    const gradeVersion = includeUngraded ? assignmentGroupGrade.final : assignmentGroupGrade.current;
    return { ...gradeVersion, weight: assignmentGroupGrade.assignmentGroupWeight };
  });

  if (options.weightAssignmentGroups) {
    const relevantGroupGrades = _.filter(scopedAssignmentGroupGrades, 'possible');
    const fullWeight = sumBy(relevantGroupGrades, 'weight');

    let finalGrade = sum(_.map(relevantGroupGrades, getWeightedPercent));
    if (fullWeight === 0) {
      finalGrade = null;
    } else if (fullWeight < 100) {
      finalGrade = (finalGrade * 100) / fullWeight;
    }

    const submissionCount = sumBy(relevantGroupGrades, 'submission_count');
    const possible = ((submissionCount > 0) || includeUngraded) ? 100 : 0;
    let score = finalGrade && round(finalGrade, 2);
    score = isNaN(score) ? null : score;

    return { score, possible };
  }

  return {
    score: sumBy(scopedAssignmentGroupGrades, 'score'),
    possible: sumBy(scopedAssignmentGroupGrades, 'possible')
  }
}

function combineGradingPeriodGrades (gradingPeriodGradesByPeriodId, includeUngraded) {
  let scopedGradingPeriodGrades = _.map(gradingPeriodGradesByPeriodId, (gradingPeriodGrade) => {
    const gradeVersion = includeUngraded ? gradingPeriodGrade.final : gradingPeriodGrade.current;
    return { ...gradeVersion, weight: gradingPeriodGrade.gradingPeriodWeight };
  });

  if (!includeUngraded) {
    scopedGradingPeriodGrades = _.filter(scopedGradingPeriodGrades, 'possible');
  }

  const weightedScores = _.map(scopedGradingPeriodGrades, getWeightedPercent);
  const totalWeight = sumBy(scopedGradingPeriodGrades, 'weight');
  const totalScore = totalWeight === 0 ? 0 : (sum(weightedScores) * 100) / Math.min(totalWeight, 100);

  return {
    score: round(totalScore, 2),
    possible: 100
  };
}

function divideGroupByGradingPeriods (assignmentGroup, effectiveDueDates) {
  // When using weighted grading periods, assignment groups must not contain assignments due in different grading
  // periods. This allows for calculated assignment group grades in closed grading periods to be accidentally
  // changed if a related assignment is considered to be in an open grading period.
  //
  // To avoid this, assignment groups meeting this criteria are "divided" (duplicated) in a way where each
  // instance of the assignment group includes assignments only from one grading period.
  const assignmentsByGradingPeriodId = _.groupBy(assignmentGroup.assignments, assignment => (
    effectiveDueDates[assignment.id].grading_period_id
  ));
  return _.map(assignmentsByGradingPeriodId, assignments => (
    { ...assignmentGroup, assignments }
  ));
}

function extractPeriodBasedAssignmentGroups (assignmentGroups, effectiveDueDates) {
  return _.reduce(assignmentGroups, (periodBasedGroups, assignmentGroup) => {
    const assignedAssignments = _.filter(assignmentGroup.assignments, assignment => (
      effectiveDueDates[assignment.id]
    ));
    if (assignedAssignments.length > 0) {
      const groupWithAssignedAssignments = { ...assignmentGroup, assignments: assignedAssignments };
      return [
        ...periodBasedGroups,
        ...divideGroupByGradingPeriods(groupWithAssignedAssignments, effectiveDueDates)
      ];
    }
    return periodBasedGroups;
  }, []);
}

function recombinePeriodBasedAssignmentGroupGrades (grades) {
  const map = {};
  _.forEach(grades, (grade) => {
    const previousGrade = map[grade.assignmentGroupId];
    if (previousGrade) {
      map[grade.assignmentGroupId] = {
        ...previousGrade,
        current: {
          submission_count: previousGrade.current.submission_count + grade.current.submission_count,
          submissions: [...previousGrade.current.submissions, ...grade.current.submissions],
          score: previousGrade.current.score + grade.current.score,
          possible: previousGrade.current.possible + grade.current.possible
        },
        final: {
          submission_count: previousGrade.final.submission_count + grade.final.submission_count,
          submissions: [...previousGrade.final.submissions, ...grade.final.submissions],
          score: previousGrade.final.score + grade.final.score,
          possible: previousGrade.final.possible + grade.final.possible
        }
      };
    } else {
      map[grade.assignmentGroupId] = grade;
    }
  });
  return map;
}

function calculateWithGradingPeriods (submissions, assignmentGroups, gradingPeriods, effectiveDueDates, options) {
  const periodBasedGroups = extractPeriodBasedAssignmentGroups(assignmentGroups, effectiveDueDates);

  const assignmentGroupsByGradingPeriodId = _.groupBy(periodBasedGroups, (assignmentGroup) => {
    const assignmentId = assignmentGroup.assignments[0].id;
    return effectiveDueDates[assignmentId].grading_period_id;
  });

  const gradingPeriodsById = _.indexBy(gradingPeriods, 'id');
  const gradingPeriodGradesByPeriodId = {};
  const periodBasedAssignmentGroupGrades = [];

  _.forEach(gradingPeriods, (gradingPeriod) => {
    const groupGrades = {};

    (assignmentGroupsByGradingPeriodId[gradingPeriod.id] || []).forEach((assignmentGroup) => {
      groupGrades[assignmentGroup.id] = AssignmentGroupGradeCalculator.calculate(submissions, assignmentGroup);
      periodBasedAssignmentGroupGrades.push(groupGrades[assignmentGroup.id]);
    });

    const groupGradesList = _.values(groupGrades);

    gradingPeriodGradesByPeriodId[gradingPeriod.id] = {
      gradingPeriodId: gradingPeriod.id,
      gradingPeriodWeight: gradingPeriodsById[gradingPeriod.id].weight || 0,
      assignmentGroups: groupGrades,
      current: combineAssignmentGroupGrades(groupGradesList, false, options),
      final: combineAssignmentGroupGrades(groupGradesList, true, options),
      scoreUnit: options.weightAssignmentGroups ? 'percentage' : 'points'
    };
  });

  if (options.weightGradingPeriods) {
    return {
      assignmentGroups: recombinePeriodBasedAssignmentGroupGrades(periodBasedAssignmentGroupGrades),
      gradingPeriods: gradingPeriodGradesByPeriodId,
      current: combineGradingPeriodGrades(gradingPeriodGradesByPeriodId, false, options),
      final: combineGradingPeriodGrades(gradingPeriodGradesByPeriodId, true, options),
      scoreUnit: 'percentage'
    };
  }

  const allAssignmentGroupGrades = _.map(assignmentGroups, assignmentGroup => (
    AssignmentGroupGradeCalculator.calculate(submissions, assignmentGroup)
  ));

  return {
    assignmentGroups: _.indexBy(allAssignmentGroupGrades, grade => grade.assignmentGroupId),
    gradingPeriods: gradingPeriodGradesByPeriodId,
    current: combineAssignmentGroupGrades(allAssignmentGroupGrades, false, options),
    final: combineAssignmentGroupGrades(allAssignmentGroupGrades, true, options),
    scoreUnit: options.weightAssignmentGroups ? 'percentage' : 'points'
  };
}

function calculateWithoutGradingPeriods (submissions, assignmentGroups, options) {
  const assignmentGroupGrades = _.map(assignmentGroups, assignmentGroup => (
    AssignmentGroupGradeCalculator.calculate(submissions, assignmentGroup)
  ));

  return {
    assignmentGroups: _.indexBy(assignmentGroupGrades, grade => grade.assignmentGroupId),
    current: combineAssignmentGroupGrades(assignmentGroupGrades, false, options),
    final: combineAssignmentGroupGrades(assignmentGroupGrades, true, options),
    scoreUnit: options.weightAssignmentGroups ? 'percentage' : 'points'
  };
}

// Each submission requires the following properties:
// * score: number
// * points_possible: non-negative integer
// * assignment_id: Canvas id
// * assignment_group_id: Canvas id
// * excused: boolean
//
// Ungraded submissions will have a score of `null`.
//
// Each assignment group requires the following properties:
// * id: Canvas id
// * rules: object *see below
// * group_weight: non-negative number
// * assignments: array *see below
//
// `rules` has the following properties:
// * drop_lowest: non-negative integer
// * drop_highest: non-negative integer
// * never_drop: [array of assignment ids]
//
// `assignments` is an array of objects with the following properties:
// * id: Canvas id
// * points_possible: non-negative number
// * submission_types: [array of strings]
//
// The weighting scheme is one of [`percent`, `points`]
//
// When weightingScheme is `percent`, assignment group weights are used.
// Otherwise, no weighting is applied.
//
// Grading period set and effective due dates are optional, but must be used
// together.
//
// `gradingPeriodSet` is an object with at least the following shape:
// * gradingPeriods: [array of grading periods *see below]
// * weight: non-negative number
//
// Each grading period requires the following properties:
// * id: Canvas id
// * weight: non-negative number
//
// `effectiveDueDates` is an object with at least the following shape:
// {
//   <assignment id (Canvas id)>: {
//     grading_period_id: <grading period id (Canvas id)>
//   }
// }
//
// `effectiveDueDates` should generally include an assignment id for most/all
// assignments in use for the course and student. The structure above is the
// "user-scoped" form of effective due dates, which includes only the
// necessary data to perform a grade calculation. Effective due date entries
// would otherwise include more information about a student's relationship
// with an assignment and related grading periods.
//
// Grades minimally have the following shape:
// {
//   score: number|null
//   possible: number|null
// }
//
// AssignmentGroup Grade maps have the following shape:
// {
//   <assignment group id (Canvas id)>: <AssignmentGroup Grade Set *see below>
// }
//
// GradingPeriod Grade Sets have the following shape:
// {
//   gradingPeriodId: <Canvas id>
//   gradingPeriodWeight: number
//   assignmentGroups: <AssignmentGroup Grade map>
//   current: <AssignmentGroup Grade *see below>
//   final: <AssignmentGroup Grade *see below>
//   scoreUnit: 'points'|'percent'
// }
//
// GradingPeriod Grade maps have the following shape:
// {
//   <grading period id (Canvas id)>: <GradingPeriod Grade Set *see above>
// }
//
// Each grading period will have a map for assignment group grades, keyed to
// the id of assignment groups graded within the grading period. Not every
// call to `calculate` will include grading period grades, as some courses do
// not use grading periods.
//
// An AssignmentGroup Grade Set is the returned result from the
// AssignmentGroupGradeCalculator.calculate function.
//
// Return value is a Course Grade Set.
// A Course Grade Set has the following shape:
// {
//   assignmentGroups: <AssignmentGroup Grade map *see above>
//   gradingPeriods: <GradingPeriod Grade map *see above>
//   current: <AssignmentGroup Grade *see above>
//   final: <AssignmentGroup Grade *see above>
//   scoreUnit: 'points'|'percent'
// }
function calculate (submissions, assignmentGroups, weightingScheme, gradingPeriodSet, effectiveDueDates) {
  const options = {
    weightGradingPeriods: gradingPeriodSet && !!gradingPeriodSet.weighted,
    weightAssignmentGroups: weightingScheme === 'percent'
  };

  if (gradingPeriodSet && effectiveDueDates) {
    return calculateWithGradingPeriods(
      submissions, assignmentGroups, gradingPeriodSet.gradingPeriods, effectiveDueDates, options
    );
  }

  return calculateWithoutGradingPeriods(submissions, assignmentGroups, options);
}

export default {
  calculate
}
<commit_msg>replace use of Number.parseFloat<commit_after>/*
 * Copyright (C) 2016 - present Instructure, Inc.
 *
 * This file is part of Canvas.
 *
 * Canvas is free software: you can redistribute it and/or modify it under
 * the terms of the GNU Affero General Public License as published by the Free
 * Software Foundation, version 3 of the License.
 *
 * Canvas 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/>.
 */

import _ from 'underscore'
import Big from 'big.js'
import round from 'compiled/util/round'
import AssignmentGroupGradeCalculator from '../gradebook/AssignmentGroupGradeCalculator'

function addWithPrecision (a, b) {
  if (a == null && b == null) {
    return new Big(0)
  } else if (a == null) {
    return new Big(b)
  } else if (b == null) {
    return new Big(a)
  } else {
    return new Big(a).plus(b)
  }
}

function sum (collection) {
  const bigValue = _.reduce(collection, addWithPrecision, 0)
  return parseFloat(bigValue, 10)
}

function sumBy (collection, attr) {
  const values = _.map(collection, attr);
  return sum(values);
}

function getWeightedPercent ({ score, possible, weight }) {
  return score ? (score / possible) * weight : 0;
}

function combineAssignmentGroupGrades (assignmentGroupGrades, includeUngraded, options) {
  const scopedAssignmentGroupGrades = _.map(assignmentGroupGrades, (assignmentGroupGrade) => {
    const gradeVersion = includeUngraded ? assignmentGroupGrade.final : assignmentGroupGrade.current;
    return { ...gradeVersion, weight: assignmentGroupGrade.assignmentGroupWeight };
  });

  if (options.weightAssignmentGroups) {
    const relevantGroupGrades = _.filter(scopedAssignmentGroupGrades, 'possible');
    const fullWeight = sumBy(relevantGroupGrades, 'weight');

    let finalGrade = sum(_.map(relevantGroupGrades, getWeightedPercent));
    if (fullWeight === 0) {
      finalGrade = null;
    } else if (fullWeight < 100) {
      finalGrade = (finalGrade * 100) / fullWeight;
    }

    const submissionCount = sumBy(relevantGroupGrades, 'submission_count');
    const possible = ((submissionCount > 0) || includeUngraded) ? 100 : 0;
    let score = finalGrade && round(finalGrade, 2);
    score = isNaN(score) ? null : score;

    return { score, possible };
  }

  return {
    score: sumBy(scopedAssignmentGroupGrades, 'score'),
    possible: sumBy(scopedAssignmentGroupGrades, 'possible')
  }
}

function combineGradingPeriodGrades (gradingPeriodGradesByPeriodId, includeUngraded) {
  let scopedGradingPeriodGrades = _.map(gradingPeriodGradesByPeriodId, (gradingPeriodGrade) => {
    const gradeVersion = includeUngraded ? gradingPeriodGrade.final : gradingPeriodGrade.current;
    return { ...gradeVersion, weight: gradingPeriodGrade.gradingPeriodWeight };
  });

  if (!includeUngraded) {
    scopedGradingPeriodGrades = _.filter(scopedGradingPeriodGrades, 'possible');
  }

  const weightedScores = _.map(scopedGradingPeriodGrades, getWeightedPercent);
  const totalWeight = sumBy(scopedGradingPeriodGrades, 'weight');
  const totalScore = totalWeight === 0 ? 0 : (sum(weightedScores) * 100) / Math.min(totalWeight, 100);

  return {
    score: round(totalScore, 2),
    possible: 100
  };
}

function divideGroupByGradingPeriods (assignmentGroup, effectiveDueDates) {
  // When using weighted grading periods, assignment groups must not contain assignments due in different grading
  // periods. This allows for calculated assignment group grades in closed grading periods to be accidentally
  // changed if a related assignment is considered to be in an open grading period.
  //
  // To avoid this, assignment groups meeting this criteria are "divided" (duplicated) in a way where each
  // instance of the assignment group includes assignments only from one grading period.
  const assignmentsByGradingPeriodId = _.groupBy(assignmentGroup.assignments, assignment => (
    effectiveDueDates[assignment.id].grading_period_id
  ));
  return _.map(assignmentsByGradingPeriodId, assignments => (
    { ...assignmentGroup, assignments }
  ));
}

function extractPeriodBasedAssignmentGroups (assignmentGroups, effectiveDueDates) {
  return _.reduce(assignmentGroups, (periodBasedGroups, assignmentGroup) => {
    const assignedAssignments = _.filter(assignmentGroup.assignments, assignment => (
      effectiveDueDates[assignment.id]
    ));
    if (assignedAssignments.length > 0) {
      const groupWithAssignedAssignments = { ...assignmentGroup, assignments: assignedAssignments };
      return [
        ...periodBasedGroups,
        ...divideGroupByGradingPeriods(groupWithAssignedAssignments, effectiveDueDates)
      ];
    }
    return periodBasedGroups;
  }, []);
}

function recombinePeriodBasedAssignmentGroupGrades (grades) {
  const map = {};
  _.forEach(grades, (grade) => {
    const previousGrade = map[grade.assignmentGroupId];
    if (previousGrade) {
      map[grade.assignmentGroupId] = {
        ...previousGrade,
        current: {
          submission_count: previousGrade.current.submission_count + grade.current.submission_count,
          submissions: [...previousGrade.current.submissions, ...grade.current.submissions],
          score: previousGrade.current.score + grade.current.score,
          possible: previousGrade.current.possible + grade.current.possible
        },
        final: {
          submission_count: previousGrade.final.submission_count + grade.final.submission_count,
          submissions: [...previousGrade.final.submissions, ...grade.final.submissions],
          score: previousGrade.final.score + grade.final.score,
          possible: previousGrade.final.possible + grade.final.possible
        }
      };
    } else {
      map[grade.assignmentGroupId] = grade;
    }
  });
  return map;
}

function calculateWithGradingPeriods (submissions, assignmentGroups, gradingPeriods, effectiveDueDates, options) {
  const periodBasedGroups = extractPeriodBasedAssignmentGroups(assignmentGroups, effectiveDueDates);

  const assignmentGroupsByGradingPeriodId = _.groupBy(periodBasedGroups, (assignmentGroup) => {
    const assignmentId = assignmentGroup.assignments[0].id;
    return effectiveDueDates[assignmentId].grading_period_id;
  });

  const gradingPeriodsById = _.indexBy(gradingPeriods, 'id');
  const gradingPeriodGradesByPeriodId = {};
  const periodBasedAssignmentGroupGrades = [];

  _.forEach(gradingPeriods, (gradingPeriod) => {
    const groupGrades = {};

    (assignmentGroupsByGradingPeriodId[gradingPeriod.id] || []).forEach((assignmentGroup) => {
      groupGrades[assignmentGroup.id] = AssignmentGroupGradeCalculator.calculate(submissions, assignmentGroup);
      periodBasedAssignmentGroupGrades.push(groupGrades[assignmentGroup.id]);
    });

    const groupGradesList = _.values(groupGrades);

    gradingPeriodGradesByPeriodId[gradingPeriod.id] = {
      gradingPeriodId: gradingPeriod.id,
      gradingPeriodWeight: gradingPeriodsById[gradingPeriod.id].weight || 0,
      assignmentGroups: groupGrades,
      current: combineAssignmentGroupGrades(groupGradesList, false, options),
      final: combineAssignmentGroupGrades(groupGradesList, true, options),
      scoreUnit: options.weightAssignmentGroups ? 'percentage' : 'points'
    };
  });

  if (options.weightGradingPeriods) {
    return {
      assignmentGroups: recombinePeriodBasedAssignmentGroupGrades(periodBasedAssignmentGroupGrades),
      gradingPeriods: gradingPeriodGradesByPeriodId,
      current: combineGradingPeriodGrades(gradingPeriodGradesByPeriodId, false, options),
      final: combineGradingPeriodGrades(gradingPeriodGradesByPeriodId, true, options),
      scoreUnit: 'percentage'
    };
  }

  const allAssignmentGroupGrades = _.map(assignmentGroups, assignmentGroup => (
    AssignmentGroupGradeCalculator.calculate(submissions, assignmentGroup)
  ));

  return {
    assignmentGroups: _.indexBy(allAssignmentGroupGrades, grade => grade.assignmentGroupId),
    gradingPeriods: gradingPeriodGradesByPeriodId,
    current: combineAssignmentGroupGrades(allAssignmentGroupGrades, false, options),
    final: combineAssignmentGroupGrades(allAssignmentGroupGrades, true, options),
    scoreUnit: options.weightAssignmentGroups ? 'percentage' : 'points'
  };
}

function calculateWithoutGradingPeriods (submissions, assignmentGroups, options) {
  const assignmentGroupGrades = _.map(assignmentGroups, assignmentGroup => (
    AssignmentGroupGradeCalculator.calculate(submissions, assignmentGroup)
  ));

  return {
    assignmentGroups: _.indexBy(assignmentGroupGrades, grade => grade.assignmentGroupId),
    current: combineAssignmentGroupGrades(assignmentGroupGrades, false, options),
    final: combineAssignmentGroupGrades(assignmentGroupGrades, true, options),
    scoreUnit: options.weightAssignmentGroups ? 'percentage' : 'points'
  };
}

// Each submission requires the following properties:
// * score: number
// * points_possible: non-negative integer
// * assignment_id: Canvas id
// * assignment_group_id: Canvas id
// * excused: boolean
//
// Ungraded submissions will have a score of `null`.
//
// Each assignment group requires the following properties:
// * id: Canvas id
// * rules: object *see below
// * group_weight: non-negative number
// * assignments: array *see below
//
// `rules` has the following properties:
// * drop_lowest: non-negative integer
// * drop_highest: non-negative integer
// * never_drop: [array of assignment ids]
//
// `assignments` is an array of objects with the following properties:
// * id: Canvas id
// * points_possible: non-negative number
// * submission_types: [array of strings]
//
// The weighting scheme is one of [`percent`, `points`]
//
// When weightingScheme is `percent`, assignment group weights are used.
// Otherwise, no weighting is applied.
//
// Grading period set and effective due dates are optional, but must be used
// together.
//
// `gradingPeriodSet` is an object with at least the following shape:
// * gradingPeriods: [array of grading periods *see below]
// * weight: non-negative number
//
// Each grading period requires the following properties:
// * id: Canvas id
// * weight: non-negative number
//
// `effectiveDueDates` is an object with at least the following shape:
// {
//   <assignment id (Canvas id)>: {
//     grading_period_id: <grading period id (Canvas id)>
//   }
// }
//
// `effectiveDueDates` should generally include an assignment id for most/all
// assignments in use for the course and student. The structure above is the
// "user-scoped" form of effective due dates, which includes only the
// necessary data to perform a grade calculation. Effective due date entries
// would otherwise include more information about a student's relationship
// with an assignment and related grading periods.
//
// Grades minimally have the following shape:
// {
//   score: number|null
//   possible: number|null
// }
//
// AssignmentGroup Grade maps have the following shape:
// {
//   <assignment group id (Canvas id)>: <AssignmentGroup Grade Set *see below>
// }
//
// GradingPeriod Grade Sets have the following shape:
// {
//   gradingPeriodId: <Canvas id>
//   gradingPeriodWeight: number
//   assignmentGroups: <AssignmentGroup Grade map>
//   current: <AssignmentGroup Grade *see below>
//   final: <AssignmentGroup Grade *see below>
//   scoreUnit: 'points'|'percent'
// }
//
// GradingPeriod Grade maps have the following shape:
// {
//   <grading period id (Canvas id)>: <GradingPeriod Grade Set *see above>
// }
//
// Each grading period will have a map for assignment group grades, keyed to
// the id of assignment groups graded within the grading period. Not every
// call to `calculate` will include grading period grades, as some courses do
// not use grading periods.
//
// An AssignmentGroup Grade Set is the returned result from the
// AssignmentGroupGradeCalculator.calculate function.
//
// Return value is a Course Grade Set.
// A Course Grade Set has the following shape:
// {
//   assignmentGroups: <AssignmentGroup Grade map *see above>
//   gradingPeriods: <GradingPeriod Grade map *see above>
//   current: <AssignmentGroup Grade *see above>
//   final: <AssignmentGroup Grade *see above>
//   scoreUnit: 'points'|'percent'
// }
function calculate (submissions, assignmentGroups, weightingScheme, gradingPeriodSet, effectiveDueDates) {
  const options = {
    weightGradingPeriods: gradingPeriodSet && !!gradingPeriodSet.weighted,
    weightAssignmentGroups: weightingScheme === 'percent'
  };

  if (gradingPeriodSet && effectiveDueDates) {
    return calculateWithGradingPeriods(
      submissions, assignmentGroups, gradingPeriodSet.gradingPeriods, effectiveDueDates, options
    );
  }

  return calculateWithoutGradingPeriods(submissions, assignmentGroups, options);
}

export default {
  calculate
}
<|endoftext|>


<commit_before>/*!
 * Copyright (c) 2015-2020 Cisco Systems, Inc. See LICENSE file.
 */

import '@webex/internal-plugin-device';
import '@webex/internal-plugin-encryption';
import '@webex/internal-plugin-conversation';

import {registerInternalPlugin} from '@webex/webex-core';
import {has} from 'lodash';

import Calendar from './calendar';
import config from './config';

registerInternalPlugin('calendar', Calendar, {
  config,
  payloadTransformer: {
    predicates: [
      {
        name: 'transformMeetingNotes',
        direction: 'inbound',
        test(ctx, response) {
          return Promise.resolve(has(response, 'body.encryptedNotes'));
        },
        extract(response) {
          return Promise.resolve(response.body);
        }
      },
      {
        name: 'transformMeetingParticipants',
        direction: 'inbound',
        test(ctx, response) {
          return Promise.resolve(has(response, 'body.encryptedParticipants'));
        },
        extract(response) {
          return Promise.resolve(response.body);
        }
      },
      {
        name: 'transformMeetingArray',
        direction: 'inbound',
        test(ctx, response) {
          return Promise.resolve(has(response, 'body.items[0].seriesId'));
        },
        extract(response) {
          return Promise.resolve(response.body.items);
        }
      },
      {
        name: 'transformMeeting',
        direction: 'inbound',
        test(ctx, response) {
          return Promise.resolve(has(response, 'body.seriesId'));
        },
        extract(response) {
          return Promise.resolve(response.body);
        }
      },
      {
        name: 'transformMeeting',
        direction: 'inbound',
        test(ctx, response) {
          return Promise.resolve(has(response, 'calendarMeetingExternal'));
        },
        extract(response) {
          return Promise.resolve(response.calendarMeetingExternal);
        }
      }
    ],
    transforms: [
      {
        name: 'transformMeetingArray',
        fn(ctx, array) {
          return Promise.all(array.map((item) => ctx.transform('transformMeeting', item)));
        }
      },
      {
        name: 'transformMeeting',
        direction: 'inbound',
        fn(ctx, object) {
          if (!object) {
            return Promise.resolve();
          }

          if (!object.encryptionKeyUrl) {
            return Promise.resolve();
          }

          // Decrypt participant properties if meeting object contains participants
          const decryptedParticipants = object.encryptedParticipants ? object.encryptedParticipants.map((participant) => Promise.all([
            ctx.transform('decryptTextProp', 'encryptedEmailAddress', object.encryptionKeyUrl, participant),
            ctx.transform('decryptTextProp', 'encryptedName', object.encryptionKeyUrl, participant)
          ])) : [];

          return Promise.all([
            ctx.transform('decryptTextProp', 'encryptedSubject', object.encryptionKeyUrl, object),
            ctx.transform('decryptTextProp', 'encryptedLocation', object.encryptionKeyUrl, object),
            ctx.transform('decryptTextProp', 'encryptedNotes', object.encryptionKeyUrl, object),
            ctx.transform('decryptTextProp', 'webexURI', object.encryptionKeyUrl, object),
            ctx.transform('decryptTextProp', 'webexURL', object.encryptionKeyUrl, object),
            ctx.transform('decryptTextProp', 'spaceMeetURL', object.encryptionKeyUrl, object),
            ctx.transform('decryptTextProp', 'spaceURI', object.encryptionKeyUrl, object),
            ctx.transform('decryptTextProp', 'spaceURL', object.encryptionKeyUrl, object)
          ].concat(decryptedParticipants));
        }
      },
      {
        name: 'transformMeetingNotes',
        direction: 'inbound',
        fn(ctx, object) {
          if (!object) {
            return Promise.resolve();
          }

          if (!object.encryptionKeyUrl) {
            return Promise.resolve();
          }

          return Promise.all([
            ctx.transform('decryptTextProp', 'encryptedNotes', object.encryptionKeyUrl, object)
          ]);
        }
      },
      {
        name: 'transformMeetingParticipants',
        direction: 'inbound',
        fn(ctx, object) {
          if (!object) {
            return Promise.resolve();
          }

          if (!object.encryptionKeyUrl || !object.encryptedParticipants) {
            return Promise.resolve();
          }

          // Decrypt participant properties
          const decryptedParticipants = object.encryptedParticipants.map((participant) => Promise.all([
            ctx.transform('decryptTextProp', 'encryptedEmailAddress', object.encryptionKeyUrl, participant),
            ctx.transform('decryptTextProp', 'encryptedName', object.encryptionKeyUrl, participant)
          ]));

          return Promise.all(decryptedParticipants);
        }
      }
    ]
  }
});

export {default} from './calendar';
<commit_msg>feat(calender): support new DTO meetingJoinInfo to populate other fields<commit_after>/*!
 * Copyright (c) 2015-2020 Cisco Systems, Inc. See LICENSE file.
 */

import '@webex/internal-plugin-device';
import '@webex/internal-plugin-encryption';
import '@webex/internal-plugin-conversation';

import {registerInternalPlugin} from '@webex/webex-core';
import {has} from 'lodash';

import Calendar from './calendar';
import config from './config';

registerInternalPlugin('calendar', Calendar, {
  config,
  payloadTransformer: {
    predicates: [
      {
        name: 'transformMeetingNotes',
        direction: 'inbound',
        test(ctx, response) {
          return Promise.resolve(has(response, 'body.encryptedNotes'));
        },
        extract(response) {
          return Promise.resolve(response.body);
        }
      },
      {
        name: 'transformMeetingParticipants',
        direction: 'inbound',
        test(ctx, response) {
          return Promise.resolve(has(response, 'body.encryptedParticipants'));
        },
        extract(response) {
          return Promise.resolve(response.body);
        }
      },
      {
        name: 'transformMeetingArray',
        direction: 'inbound',
        test(ctx, response) {
          return Promise.resolve(has(response, 'body.items[0].seriesId'));
        },
        extract(response) {
          return Promise.resolve(response.body.items);
        }
      },
      {
        name: 'transformMeeting',
        direction: 'inbound',
        test(ctx, response) {
          return Promise.resolve(has(response, 'body.seriesId'));
        },
        extract(response) {
          return Promise.resolve(response.body);
        }
      },
      {
        name: 'transformMeeting',
        direction: 'inbound',
        test(ctx, response) {
          return Promise.resolve(has(response, 'calendarMeetingExternal'));
        },
        extract(response) {
          return Promise.resolve(response.calendarMeetingExternal);
        }
      }
    ],
    transforms: [
      {
        name: 'transformMeetingArray',
        fn(ctx, array) {
          return Promise.all(array.map((item) => ctx.transform('transformMeeting', item)));
        }
      },
      {
        name: 'transformMeeting',
        direction: 'inbound',
        fn(ctx, object) {
          if (!object) {
            return Promise.resolve();
          }

          if (!object.encryptionKeyUrl) {
            return Promise.resolve();
          }

          // Decrypt participant properties if meeting object contains participants
          const decryptedParticipants = object.encryptedParticipants ? object.encryptedParticipants.map((participant) => Promise.all([
            ctx.transform('decryptTextProp', 'encryptedEmailAddress', object.encryptionKeyUrl, participant),
            ctx.transform('decryptTextProp', 'encryptedName', object.encryptionKeyUrl, participant)
          ])) : [];

          return Promise.all([
            ctx.transform('decryptTextProp', 'encryptedSubject', object.encryptionKeyUrl, object),
            ctx.transform('decryptTextProp', 'encryptedLocation', object.encryptionKeyUrl, object),
            ctx.transform('decryptTextProp', 'encryptedNotes', object.encryptionKeyUrl, object),
            ctx.transform('decryptTextProp', 'webexURI', object.encryptionKeyUrl, object),
            ctx.transform('decryptTextProp', 'webexURL', object.encryptionKeyUrl, object),
            ctx.transform('decryptTextProp', 'spaceMeetURL', object.encryptionKeyUrl, object),
            ctx.transform('decryptTextProp', 'spaceURI', object.encryptionKeyUrl, object),
            ctx.transform('decryptTextProp', 'spaceURL', object.encryptionKeyUrl, object),
            ctx.transform('decryptTextProp', 'meetingJoinURI', object.encryptionKeyUrl, object.meetingJoinInfo),
            ctx.transform('decryptTextProp', 'meetingJoinURL', object.encryptionKeyUrl, object.meetingJoinInfo)
          ].concat(decryptedParticipants));
        }
      },
      {
        name: 'transformMeetingNotes',
        direction: 'inbound',
        fn(ctx, object) {
          if (!object) {
            return Promise.resolve();
          }

          if (!object.encryptionKeyUrl) {
            return Promise.resolve();
          }

          return Promise.all([
            ctx.transform('decryptTextProp', 'encryptedNotes', object.encryptionKeyUrl, object)
          ]);
        }
      },
      {
        name: 'transformMeetingParticipants',
        direction: 'inbound',
        fn(ctx, object) {
          if (!object) {
            return Promise.resolve();
          }

          if (!object.encryptionKeyUrl || !object.encryptedParticipants) {
            return Promise.resolve();
          }

          // Decrypt participant properties
          const decryptedParticipants = object.encryptedParticipants.map((participant) => Promise.all([
            ctx.transform('decryptTextProp', 'encryptedEmailAddress', object.encryptionKeyUrl, participant),
            ctx.transform('decryptTextProp', 'encryptedName', object.encryptionKeyUrl, participant)
          ]));

          return Promise.all(decryptedParticipants);
        }
      }
    ]
  }
});

export {default} from './calendar';
<|endoftext|>


<commit_before>import cx from 'classnames';
import React, {
  useState,
  useReducer,
  useEffect,
  useRef,
  useCallback
} from 'react';
import { StockFlux } from 'stockflux-core';
import reducer, {
  initialSearchState
} from '../reducers/free-text-search/FreeTextSearch';
import searchAction from '../reducers/free-text-search/Action';
import {
  ScreenEdge,
  useBounds,
  useInterApplicationBusSubscribe
} from 'openfin-react-hooks';
import { OpenfinApiHelpers } from 'stockflux-core/src';
import getResultsWindowProps from '../search-results/helpers/GetResultsWindowProps';
import ChildWindow from '../search-results/ChildWindow';
import SearchInputField from './SearchInputField';
import SearchButton from './SearchButton';
import { useChildWindow } from 'openfin-react-hooks';
import './FreeTextSearch.css';

const ALL = { uuid: '*' };
const SEARCH_TIMEOUT_INTERVAL = 250;
const SEARCH_RESULTS_WINDOW_NAME = 'search-results';
const SEARCH_RESULTS_CSS_PATCH = 'childWindow.css';

let latestRequest = null;
const FreeTextSearch = ({ dockedTo, showTextInput }) => {
  const [searchState, dispatch] = useReducer(reducer, initialSearchState);
  const [query, setQuery] = useState(null);
  const [parentUuid, setParentUuid] = useState(null);
  const bounds = useBounds();
  const [debouncedQuery, setDebouncedQuery] = useState(null);
  const childWindow = useChildWindow({
    name: SEARCH_RESULTS_WINDOW_NAME,
    parentDocument: document,
    cssUrl: SEARCH_RESULTS_CSS_PATCH,
    shouldInheritCss: true,
    shouldClosePreviousOnLaunch: true
  });
  const { isSearching, results } = searchState;
  const searchButtonRef = useRef(null);
  const launcherInputRef = useRef(null);
  const childWindowInputRef = useRef(null);

  const { windowRef, launch, populate, close } = childWindow;

  useEffect(() => {
    OpenfinApiHelpers.getCurrentWindowOptions().then(options =>
      setParentUuid(options.uuid)
    );
  }, []);

  const isDockedToSide = [ScreenEdge.LEFT, ScreenEdge.RIGHT].includes(dockedTo);
  const isDocked = dockedTo !== ScreenEdge.NONE;

  const launchChildWindow = useCallback(
    () =>
      launch(
        getResultsWindowProps(
          SEARCH_RESULTS_WINDOW_NAME,
          searchButtonRef,
          isDockedToSide ? childWindowInputRef : launcherInputRef,
          dockedTo,
          bounds
        )
      ),
    [bounds, dockedTo, launch, isDockedToSide]
  );

  const closeChildWindow = useCallback(() => {
    setQuery(null);
    dispatch({ type: searchAction.INITIALISE });
    close();
  }, [close]);

  const handleOnInputChange = useCallback(
    event => {
      if (!windowRef) launchChildWindow();
      setQuery(event.target.value);
    },
    [windowRef, launchChildWindow]
  );

  const handleSearchClick = useCallback(() => {
    if (windowRef && results) {
      closeChildWindow();
    } else launchChildWindow();
  }, [windowRef, results, launchChildWindow, closeChildWindow]);

  useEffect(() => {
    const stockFluxSearch = () => {
      if (debouncedQuery) {
        dispatch({ type: searchAction.SEARCHING });
        try {
          const currentRequest = StockFlux.stockFluxSearch(debouncedQuery).then(
            stockFluxResults => {
              if (latestRequest === currentRequest) {
                dispatch({
                  type: searchAction.SUCCESS,
                  results: stockFluxResults
                });
              }
            }
          );
          latestRequest = currentRequest;
        } catch {
          dispatch({ type: searchAction.ERROR });
        }
      }
    };
    stockFluxSearch();
  }, [debouncedQuery]);

  useEffect(() => {
    const handler = setTimeout(
      () => setDebouncedQuery(query),
      SEARCH_TIMEOUT_INTERVAL
    );
    return () => clearTimeout(handler);
  }, [query]);

  useEffect(() => {
    if (query === '' && windowRef) {
      closeChildWindow();
    }
  }, [windowRef, query, closeChildWindow]);

  useEffect(() => {
    if (childWindow) {
      closeChildWindow();
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [dockedTo]);

  useEffect(() => {
    if (windowRef) {
      const childWindowJsx = (
        <ChildWindow
          results={results}
          isSearching={isSearching}
          debouncedQuery={debouncedQuery}
        >
          {(isDockedToSide || showTextInput) && (
            <div
              className={cx('free-text-search', {
                'search-undocked': !isDocked
              })}
            >
              <SearchInputField
                query={query ? query : ''}
                handleOnInputChange={handleOnInputChange}
                inputRef={childWindowInputRef}
              />
            </div>
          )}
        </ChildWindow>
      );
      populate(childWindowJsx);
    }
  }, [
    windowRef,
    populate,
    dockedTo,
    debouncedQuery,
    isSearching,
    results,
    handleOnInputChange,
    isDockedToSide,
    query,
    showTextInput,
    isDocked
  ]);

  const {
    data,
    subscribeError,
    isSubscribed
  } = useInterApplicationBusSubscribe(
    { uuid: parentUuid ? parentUuid : ALL },
    'search-request'
  );

  if (
    !subscribeError &&
    isSubscribed &&
    data &&
    debouncedQuery !== data.message &&
    dockedTo !== ScreenEdge.TOP
  ) {
    setDebouncedQuery(data.message);
  }

  return (
    <div
      className={cx('free-text-search', {
        'search-undocked': !isDocked
      })}
    >
      <SearchButton
        searchButtonRef={searchButtonRef}
        handleSearchClick={handleSearchClick}
      />
      {!isDockedToSide && (
        <SearchInputField
          handleOnInputChange={handleOnInputChange}
          inputRef={launcherInputRef}
        />
      )}
    </div>
  );
};
export default FreeTextSearch;
<commit_msg>stop close window on text delete if side docked<commit_after>import cx from 'classnames';
import React, {
  useState,
  useReducer,
  useEffect,
  useRef,
  useCallback
} from 'react';
import { StockFlux } from 'stockflux-core';
import reducer, {
  initialSearchState
} from '../reducers/free-text-search/FreeTextSearch';
import searchAction from '../reducers/free-text-search/Action';
import {
  ScreenEdge,
  useBounds,
  useInterApplicationBusSubscribe
} from 'openfin-react-hooks';
import { OpenfinApiHelpers } from 'stockflux-core/src';
import getResultsWindowProps from '../search-results/helpers/GetResultsWindowProps';
import ChildWindow from '../search-results/ChildWindow';
import SearchInputField from './SearchInputField';
import SearchButton from './SearchButton';
import { useChildWindow } from 'openfin-react-hooks';
import './FreeTextSearch.css';

const ALL = { uuid: '*' };
const SEARCH_TIMEOUT_INTERVAL = 250;
const SEARCH_RESULTS_WINDOW_NAME = 'search-results';
const SEARCH_RESULTS_CSS_PATCH = 'childWindow.css';

let latestRequest = null;
const FreeTextSearch = ({ dockedTo, showTextInput }) => {
  const [searchState, dispatch] = useReducer(reducer, initialSearchState);
  const [query, setQuery] = useState(null);
  const [parentUuid, setParentUuid] = useState(null);
  const bounds = useBounds();
  const [debouncedQuery, setDebouncedQuery] = useState(null);
  const childWindow = useChildWindow({
    name: SEARCH_RESULTS_WINDOW_NAME,
    parentDocument: document,
    cssUrl: SEARCH_RESULTS_CSS_PATCH,
    shouldInheritCss: true,
    shouldClosePreviousOnLaunch: true
  });
  const { isSearching, results } = searchState;
  const searchButtonRef = useRef(null);
  const launcherInputRef = useRef(null);
  const childWindowInputRef = useRef(null);

  const { windowRef, launch, populate, close } = childWindow;

  useEffect(() => {
    OpenfinApiHelpers.getCurrentWindowOptions().then(options =>
      setParentUuid(options.uuid)
    );
  }, []);

  const isDockedToSide = [ScreenEdge.LEFT, ScreenEdge.RIGHT].includes(dockedTo);
  const isDocked = dockedTo !== ScreenEdge.NONE;

  const launchChildWindow = useCallback(
    () =>
      launch(
        getResultsWindowProps(
          SEARCH_RESULTS_WINDOW_NAME,
          searchButtonRef,
          isDockedToSide ? childWindowInputRef : launcherInputRef,
          dockedTo,
          bounds
        )
      ),
    [bounds, dockedTo, launch, isDockedToSide]
  );

  const closeChildWindow = useCallback(() => {
    setQuery(null);
    dispatch({ type: searchAction.INITIALISE });
    close();
  }, [close]);

  const handleOnInputChange = useCallback(
    event => {
      if (!windowRef) launchChildWindow();
      setQuery(event.target.value);
    },
    [windowRef, launchChildWindow]
  );

  const handleSearchClick = useCallback(() => {
    if (windowRef && results) {
      closeChildWindow();
    } else launchChildWindow();
  }, [windowRef, results, launchChildWindow, closeChildWindow]);

  useEffect(() => {
    const stockFluxSearch = () => {
      if (debouncedQuery) {
        dispatch({ type: searchAction.SEARCHING });
        try {
          const currentRequest = StockFlux.stockFluxSearch(debouncedQuery).then(
            stockFluxResults => {
              if (latestRequest === currentRequest) {
                dispatch({
                  type: searchAction.SUCCESS,
                  results: stockFluxResults
                });
              }
            }
          );
          latestRequest = currentRequest;
        } catch {
          dispatch({ type: searchAction.ERROR });
        }
      }
    };
    stockFluxSearch();
  }, [debouncedQuery]);

  useEffect(() => {
    const handler = setTimeout(
      () => setDebouncedQuery(query),
      SEARCH_TIMEOUT_INTERVAL
    );
    return () => clearTimeout(handler);
  }, [query]);

  useEffect(() => {
    if (query === '' && !isDockedToSide && windowRef) {
      closeChildWindow();
    }
  }, [windowRef, query, closeChildWindow, isDockedToSide]);

  useEffect(() => {
    if (childWindow) {
      closeChildWindow();
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [dockedTo]);

  useEffect(() => {
    if (windowRef) {
      const childWindowJsx = (
        <ChildWindow
          results={results}
          isSearching={isSearching}
          debouncedQuery={debouncedQuery}
        >
          {(isDockedToSide || showTextInput) && (
            <div
              className={cx('free-text-search', {
                'search-undocked': !isDocked
              })}
            >
              <SearchInputField
                query={query ? query : ''}
                handleOnInputChange={handleOnInputChange}
                inputRef={childWindowInputRef}
              />
            </div>
          )}
        </ChildWindow>
      );
      populate(childWindowJsx);
    }
  }, [
    windowRef,
    populate,
    dockedTo,
    debouncedQuery,
    isSearching,
    results,
    handleOnInputChange,
    isDockedToSide,
    query,
    showTextInput,
    isDocked
  ]);

  const {
    data,
    subscribeError,
    isSubscribed
  } = useInterApplicationBusSubscribe(
    { uuid: parentUuid ? parentUuid : ALL },
    'search-request'
  );

  if (
    !subscribeError &&
    isSubscribed &&
    data &&
    debouncedQuery !== data.message &&
    dockedTo !== ScreenEdge.TOP
  ) {
    setDebouncedQuery(data.message);
  }

  return (
    <div
      className={cx('free-text-search', {
        'search-undocked': !isDocked
      })}
    >
      <SearchButton
        searchButtonRef={searchButtonRef}
        handleSearchClick={handleSearchClick}
      />
      {!isDockedToSide && (
        <SearchInputField
          handleOnInputChange={handleOnInputChange}
          inputRef={launcherInputRef}
        />
      )}
    </div>
  );
};
export default FreeTextSearch;
<|endoftext|>


<commit_before>import React from 'react';
import { Box, Row, Button, Col, Text, BaseImage } from '@tlon/indigo-react';
import { Link } from 'react-router-dom';
import { resourceFromPath } from '~/logic/lib/group';

export const AddFeedBanner = (props) => {
  const {
    api,
    group,
    groupPath,
  } = props;

  const disableFeed = () => {
    if (!groupPath) {
      console.error('no group path, cannot enable feed');
      return;
    }
    const resource = resourceFromPath(groupPath);
    if (!resource) {
      console.error('cannot make resource, cannot enable feed');
      return;
    }

    api.graph.disableGroupFeed(resource);
  };

  return (
    <Row
      height="48px"
      width="100%"
      alignItems="center"
      justifyContent="space-between"
      borderBottom={1}
      borderColor="lightGray"
      pl={2}
      pr={2}
    >
      <Text verticalAlign="middle">Enable Group Feed?</Text>
      <Row>
        <Text mr="2" color="gray" bold cursor="pointer" onClick={disableFeed}>
          Dismiss
        </Text>
        <Link replace to={`/~landscape${groupPath}/enable`}>
          <Button color="blue" bold cursor="pointer">
            Enable Feed
          </Button>
        </Link>
      </Row>
    </Row>
  );
};
<commit_msg>AddFeedBanner: update design to match spec<commit_after>import React, { useState } from 'react';
import { Box, Row, Button, Col, Text, BaseImage, Icon } from '@tlon/indigo-react';
import { Link } from 'react-router-dom';
import { resourceFromPath } from '~/logic/lib/group';

export const AddFeedBanner = (props) => {
  const {
    api,
    group,
    groupPath,
  } = props;
  const [dismissing, setDismissing] = useState(false);

  const disableFeed = () => {
    if (!groupPath) {
      console.error('no group path, cannot enable feed');
      return;
    }
    const resource = resourceFromPath(groupPath);
    if (!resource) {
      console.error('cannot make resource, cannot enable feed');
      return;
    }

    api.graph.disableGroupFeed(resource);
  };

  return (
    <Row
      height="48px"
      width="100%"
      alignItems="center"
      justifyContent="space-between"
      borderBottom={1}
      borderColor="lightGray"
      pl={2}
      pr={2}
    >
      <Row gapX="2">
        { dismissing ? (
          <>
            <Icon icon="Info" />
            <Text>You can always enable group feed from settings</Text>
          </>

        ) : (
          <>
            <Text fontWeight="medium" verticalAlign="middle">
              Enable Group Feed?
            </Text>
            <Text gray>
              A central place to broadcast short posts with your group
            </Text>
          </>
        )}
      </Row>
      <Row gapX="2">
        { dismissing ? (
          <Button primary onClick={disableFeed}>
            OK
          </Button>
        ) : (
          <>
            <Button onClick={() => setDismissing(true)}>
              Dismiss
            </Button>
            <Link replace to={`/~landscape${groupPath}/enable`}>
              <Button primary cursor="pointer">
                Enable Feed
              </Button>
            </Link>
          </>
        )}

      </Row>
    </Row>
  );
};
<|endoftext|>


<commit_before>// @flow
import type {
  DeleteCommand,
  IdQuery,
  IdsQuery,
  IdUpdateCommand,
  InsertCommand,
  MultiUpdateCommand,
  RestorableEntity,
  UpdateCommand,
  WhereQuery,
} from 'phenyl-interfaces'

import { sortByNotation } from 'phenyl-utils/jsnext'
import { filter } from 'power-filter/jsnext'
import { assign } from 'power-assign/jsnext'

type PlainPhenylState = {
  entities: { [name: string]: { [key: string]: RestorableEntity } }
}

/**
 *
 */
export default class PhenylState {
  entities: { [name: string]: { [key: string]: RestorableEntity } }

  constructor(plain: PlainPhenylState) {
    this.entities = plain.entities
  }

  getAll(entityName: string): Array<RestorableEntity> {
    const entities = this.entities[entityName]
    return Object.keys(entities).map(key => entities[key])
  }

  /**
   *
   */
  find(query: WhereQuery): Array<RestorableEntity> {
    const {
      entityName,
      where,
      sort,
      skip,
      limit,
    } = query
    let filtered = filter(this.getAll(entityName), where)
    if (sort != null) {
      filtered = sortByNotation(filtered, sort)
    }
    return filtered.slice(skip || 0, limit != null ? limit : filtered.length)
  }

  /**
   *
   */
  findOne(query: WhereQuery): ?RestorableEntity {
    return this.find(query)[0]
  }

  /**
   *
   */
  get(query: IdQuery): RestorableEntity {
    const entity = this.entities[query.entityName][query.id]
    if (entity == null) throw new Error('NoId')
    return entity
  }

  /**
   *
   */
  $update(command: UpdateCommand): PhenylState {
    if (command.where) {
      return this.$updateByWhereCondition(command)
    }
    return this.$updateById(command)
  }

  $updateById(command: IdUpdateCommand): PhenylState {
    // TODO
    return this
  }

  $updateByWhereCondition(command: MultiUpdateCommand): PhenylState {
    // TODO
    return this
  }

  /**
   *
   */
  $delete(command: DeleteCommand): PhenylState {
    return assign(this, { $set: {} })
  }

  /**
   * TODO rename insert register
   */
  $insert(command: InsertCommand): PhenylState {
    const { entityName } = command
    //$FlowIssue(union-type)
    const values = command.values ? command.values : [command.value]
    const operators = {}

    //$FlowIssue(values is array)
    values.forEach(value => {
      const key = Object.keys(value)[0]
      operators[`entities.${entityName}.${key}`] = value[key]
    })

    return assign(this, { $set: operators })
  }

  /**
   *
   */
  $unregister(command: InsertCommand): PhenylState {
    const { entityName } = command
    //$FlowIssue(union-type)
    const values = command.values ? command.values : [command.value]
    const operators = {}

    //$FlowIssue(values is array)
    values.forEach(value => {
      const key = Object.keys(value)[0]
      operators[`entities.${entityName}.${key}`] = value[key]
    })

    return assign(this, { $set: operators })
  }
}
<commit_msg>[phenyl-state] implement PhenylState<commit_after>// @flow
import type {
  DeleteCommand,
  IdQuery,
  IdsQuery,
  IdUpdateCommand,
  IdDeleteCommand,
  MultiUpdateCommand,
  MultiDeleteCommand,
  RestorableEntity,
  UpdateCommand,
  WhereQuery,
} from 'phenyl-interfaces'

import { sortByNotation } from 'phenyl-utils/jsnext'
import { filter } from 'power-filter/jsnext'
import { assignToProp, unassignProp } from 'power-assign/jsnext'

type PlainPhenylState = {
  entities: { [name: string]: { [key: string]: RestorableEntity } }
}

/**
 *
 */
export default class PhenylState {
  entities: { [entityName: string]: { [id: string]: RestorableEntity } }

  constructor(plain: PlainPhenylState) {
    this.entities = plain.entities
  }

  getAll(entityName: string): Array<RestorableEntity> {
    const entities = this.entities[entityName]
    return Object.keys(entities).map(key => entities[key])
  }

  /**
   *
   */
  find(query: WhereQuery): Array<RestorableEntity> {
    const {
      entityName,
      where,
      sort,
      skip,
      limit,
    } = query
    let filtered = filter(this.getAll(entityName), where)
    if (sort != null) {
      filtered = sortByNotation(filtered, sort)
    }
    return filtered.slice(skip || 0, limit != null ? limit : filtered.length)
  }

  /**
   *
   */
  findOne(query: WhereQuery): ?RestorableEntity {
    return this.find(query)[0]
  }

  /**
   *
   */
  get(query: IdQuery): RestorableEntity {
    const entity = this.entities[query.entityName][query.id]
    if (entity == null) throw new Error('NoId')
    return entity
  }

  /**
   *
   */
  getByIds(query: IdsQuery): Array<RestorableEntity> {
    const { ids, entityName } = query
    // TODO: handle error
    return ids.map(id => this.get({ entityName, id }))
  }

  /**
   *
   */
  $update(command: UpdateCommand): PhenylState {
    if (command.where) {
      return this.$updateByWhereCondition(command)
    }
    return this.$updateById(command)
  }

  /**
   *
   */
  $updateById(command: IdUpdateCommand): PhenylState {
    const { id, entityName, operators } = command
    const thisPropName = ['entities', entityName, id].join('.')
    return assignToProp(this, thisPropName, operators)
  }

  /**
   *
   */
  $updateByWhereCondition(command: MultiUpdateCommand): PhenylState {
    const { where, entityName, operators } = command
    const targetEntities = this.find({ entityName, where })
    return targetEntities.reduce((self, targetEntity) => {
      const thisPropName = ['entities', entityName, targetEntity.id].join('.')
      return assignToProp(self, thisPropName, operators)
    }, this)
  }

  /**
   * Register entities.
   * As RestorablePreEntities in InsertCommand does not have "id",
   * PhenylState cannot handle InsertCommand.
   * Instead, it receives in entities created in server.
   */
  $register(entityName: string, ...entities: Array<RestorableEntity>): PhenylState {
    return entities.reduce((self, entity) => {
      const thisPropName = ['entities', entityName, entity.id].join('.')
      return assignToProp(self, thisPropName, { $set: entity })
    }, this)
  }

  /**
   *
   */
  $delete(command: DeleteCommand): PhenylState {
    if (command.where) {
      return this.$deleteByWhereConditions(command)
    }
    return this.$deleteById(command)
  }

  /**
   *
   */
  $deleteById(command: IdDeleteCommand): PhenylState {
    const { id, entityName } = command
    return unassignProp(this, ['entities', entityName, id].join('.'))
  }

  /**
   *
   */
  $deleteByWhereConditions(command: MultiDeleteCommand): PhenylState {
    const { where, entityName } = command
    const targetEntities = this.find({ entityName, where })
    return targetEntities.reduce((self, targetEntity) => {
      const thisPropName = ['entities', entityName, targetEntity.id].join('.')
      return unassignProp(self, thisPropName)
    }, this)
  }
}
<|endoftext|>


<commit_before>import { translate } from '../../common/i18n';
import { generateLiveApiInstance } from '../../common/appId';

const CRYPTO_CURRENCIES = ['BTC', 'ETH', 'LTC', 'BCH'];

const config = {
    lists: {
        PAYOUTTYPE: [
            // [translate('Payout'), 'payout'],
            [translate('Stake'), 'stake'],
        ],
        CRYPTO_CURRENCIES,
        DETAILS: [
            [translate('statement'), '1'],
            [translate('ask price'), '2'],
            [translate('payout'), '3'],
            [translate('profit'), '4'],
            [translate('contract type'), '5'],
            [translate('entry spot'), '6'],
            [translate('entry value'), '7'],
            [translate('exit spot'), '8'],
            [translate('exit value'), '9'],
            [translate('barrier'), '10'],
            [translate('result'), '11'],
        ],
        CHECK_RESULT     : [[translate('Win'), 'win'], [translate('Loss'), 'loss']],
        CHECK_DIRECTION  : [[translate('Rise'), 'rise'], [translate('Fall'), 'fall'], [translate('No Change'), '']],
        BALANCE_TYPE     : [[translate('string'), 'STR'], [translate('number'), 'NUM']],
        NOTIFICATION_TYPE: [
            [translate('green'), 'success'],
            [translate('blue'), 'info'],
            [translate('yellow'), 'warn'],
            [translate('red'), 'error'],
        ],
        NOTIFICATION_SOUND: [
            [translate('Silent'), 'silent'],
            [translate('Announcement'), 'announcement'],
            [translate('Earned money'), 'earned-money'],
            [translate('Job done'), 'job-done'],
            [translate('Error'), 'error'],
            [translate('Severe error'), 'severe-error'],
        ],
    },

    opposites: {
        RISEFALL: [
            {
                CALL: translate('Rise'),
            },
            {
                PUT: translate('Fall'),
            },
        ],
        HIGHERLOWER: [
            {
                CALL: translate('Higher'),
            },
            {
                PUT: translate('Lower'),
            },
        ],
        TOUCHNOTOUCH: [
            {
                ONETOUCH: translate('Touch'),
            },
            {
                NOTOUCH: translate('No Touch'),
            },
        ],
        ENDSINOUT: [
            {
                EXPIRYRANGE: translate('Ends In'),
            },
            {
                EXPIRYMISS: translate('Ends Out'),
            },
        ],
        STAYSINOUT: [
            {
                RANGE: translate('Stays In'),
            },
            {
                UPORDOWN: translate('Goes Out'),
            },
        ],
        ASIANS: [
            {
                ASIANU: translate('Asian Up'),
            },
            {
                ASIAND: translate('Asian Down'),
            },
        ],
        MATCHESDIFFERS: [
            {
                DIGITMATCH: translate('Matches'),
            },
            {
                DIGITDIFF: translate('Differs'),
            },
        ],
        EVENODD: [
            {
                DIGITEVEN: translate('Even'),
            },
            {
                DIGITODD: translate('Odd'),
            },
        ],
        OVERUNDER: [
            {
                DIGITOVER: translate('Over'),
            },
            {
                DIGITUNDER: translate('Under'),
            },
        ],
    },
    barrierTypes: [['+', '+'], ['-', '-']],
    ohlcFields  : [
        [translate('Open'), 'open'],
        [translate('High'), 'high'],
        [translate('Low'), 'low'],
        [translate('Close'), 'close'],
        [translate('Open Time'), 'epoch'],
    ],
    candleIntervals: [
        [translate('Default'), 'default'],
        ['1 minute', '60'],
        ['2 minutes', '120'],
        ['3 minutes', '180'],
        ['5 minutes', '300'],
        ['10 minutes', '600'],
        ['15 minutes', '900'],
        ['30 minutes', '1800'],
        ['1 hour', '3600'],
        ['2 hours', '7200'],
        ['4 hours', '14400'],
        ['8 hours', '28800'],
        ['1 day', '86400'],
    ],
    mainBlocks   : ['trade', 'before_purchase', 'after_purchase', 'during_purchase'],
    durationTypes: {
        RISEFALL: [
            [translate('Ticks'), 't'],
            [translate('Seconds'), 's'],
            [translate('Minutes'), 'm'],
            [translate('Hours'), 'h'],
        ],
        HIGHERLOWER: [
            [translate('Ticks'), 't'],
            [translate('Seconds'), 's'],
            [translate('Minutes'), 'm'],
            [translate('Hours'), 'h'],
        ],
        TOUCHNOTOUCH  : [[translate('Minutes'), 'm'], [translate('Hours'), 'h']],
        ENDSINOUT     : [[translate('Minutes'), 'm'], [translate('Hours'), 'h']],
        STAYSINOUT    : [[translate('Minutes'), 'm'], [translate('Hours'), 'h']],
        ASIANS        : [[translate('Ticks'), 't']],
        MATCHESDIFFERS: [[translate('Ticks'), 't']],
        EVENODD       : [[translate('Ticks'), 't']],
        OVERUNDER     : [[translate('Ticks'), 't']],
    },
    hasPrediction         : ['MATCHESDIFFERS', 'OVERUNDER'],
    hasBarrierOffset      : ['HIGHERLOWER', 'TOUCHNOTOUCH', 'ENDSINOUT', 'STAYSINOUT'],
    hasSecondBarrierOffset: ['ENDSINOUT', 'STAYSINOUT'],
    conditionsCategory    : {
        callput     : ['risefall', 'higherlower'],
        touchnotouch: ['touchnotouch'],
        endsinout   : ['endsinout'],
        staysinout  : ['staysinout'],
        asian       : ['asians'],
        digits      : ['matchesdiffers', 'evenodd', 'overunder'],
    },
    conditionsCategoryName: {
        callput     : translate('Up/Down'),
        asian       : translate('Asians'),
        digits      : translate('Digits'),
        touchnotouch: translate('Touch/No Touch'),
        endsinout   : translate('Ends In/Out'),
        staysinout  : translate('Stays In/Goes Out'),
    },
    conditions: [
        'risefall',
        'higherlower',
        'touchnotouch',
        'endsinout',
        'staysinout',
        'asians',
        'matchesdiffers',
        'evenodd',
        'overunder',
    ],
    scopeNames: {
        before_purchase: translate('Before Purchase'),
        during_purchase: translate('During Purchase'),
        after_purchase : translate('After Purchase'),
        tick_analysis  : translate('Tick Analysis'),
        timeout        : translate('Run After n Seconds'),
        interval       : translate('Run Every n Seconds'),
    },
    bbResult  : [[translate('upper'), '1'], [translate('middle'), '0'], [translate('lower'), '2']],
    macdFields: [[translate('Histogram'), '0'], [translate('MACD'), '1'], [translate('Signal'), '2']],
};

export async function updateConfigCurrencies() {
    const api = generateLiveApiInstance();
    try {
        const response = await api.getPayoutCurrencies();
        config.lists.CURRENCY = response.payout_currencies.map(c => [c, c]);
    } catch (e) {
        config.lists.CURRENCY = ['USD', 'EUR', 'GBP', 'AUD', ...CRYPTO_CURRENCIES].map(c => [c, c]);
    }
}

export default config;
<commit_msg>Add ticks to touch/no-touch<commit_after>import { translate } from '../../common/i18n';
import { generateLiveApiInstance } from '../../common/appId';

const CRYPTO_CURRENCIES = ['BTC', 'ETH', 'LTC', 'BCH'];

const config = {
    lists: {
        PAYOUTTYPE: [
            // [translate('Payout'), 'payout'],
            [translate('Stake'), 'stake'],
        ],
        CRYPTO_CURRENCIES,
        DETAILS: [
            [translate('statement'), '1'],
            [translate('ask price'), '2'],
            [translate('payout'), '3'],
            [translate('profit'), '4'],
            [translate('contract type'), '5'],
            [translate('entry spot'), '6'],
            [translate('entry value'), '7'],
            [translate('exit spot'), '8'],
            [translate('exit value'), '9'],
            [translate('barrier'), '10'],
            [translate('result'), '11'],
        ],
        CHECK_RESULT     : [[translate('Win'), 'win'], [translate('Loss'), 'loss']],
        CHECK_DIRECTION  : [[translate('Rise'), 'rise'], [translate('Fall'), 'fall'], [translate('No Change'), '']],
        BALANCE_TYPE     : [[translate('string'), 'STR'], [translate('number'), 'NUM']],
        NOTIFICATION_TYPE: [
            [translate('green'), 'success'],
            [translate('blue'), 'info'],
            [translate('yellow'), 'warn'],
            [translate('red'), 'error'],
        ],
        NOTIFICATION_SOUND: [
            [translate('Silent'), 'silent'],
            [translate('Announcement'), 'announcement'],
            [translate('Earned money'), 'earned-money'],
            [translate('Job done'), 'job-done'],
            [translate('Error'), 'error'],
            [translate('Severe error'), 'severe-error'],
        ],
    },

    opposites: {
        RISEFALL: [
            {
                CALL: translate('Rise'),
            },
            {
                PUT: translate('Fall'),
            },
        ],
        HIGHERLOWER: [
            {
                CALL: translate('Higher'),
            },
            {
                PUT: translate('Lower'),
            },
        ],
        TOUCHNOTOUCH: [
            {
                ONETOUCH: translate('Touch'),
            },
            {
                NOTOUCH: translate('No Touch'),
            },
        ],
        ENDSINOUT: [
            {
                EXPIRYRANGE: translate('Ends In'),
            },
            {
                EXPIRYMISS: translate('Ends Out'),
            },
        ],
        STAYSINOUT: [
            {
                RANGE: translate('Stays In'),
            },
            {
                UPORDOWN: translate('Goes Out'),
            },
        ],
        ASIANS: [
            {
                ASIANU: translate('Asian Up'),
            },
            {
                ASIAND: translate('Asian Down'),
            },
        ],
        MATCHESDIFFERS: [
            {
                DIGITMATCH: translate('Matches'),
            },
            {
                DIGITDIFF: translate('Differs'),
            },
        ],
        EVENODD: [
            {
                DIGITEVEN: translate('Even'),
            },
            {
                DIGITODD: translate('Odd'),
            },
        ],
        OVERUNDER: [
            {
                DIGITOVER: translate('Over'),
            },
            {
                DIGITUNDER: translate('Under'),
            },
        ],
    },
    barrierTypes: [['+', '+'], ['-', '-']],
    ohlcFields  : [
        [translate('Open'), 'open'],
        [translate('High'), 'high'],
        [translate('Low'), 'low'],
        [translate('Close'), 'close'],
        [translate('Open Time'), 'epoch'],
    ],
    candleIntervals: [
        [translate('Default'), 'default'],
        ['1 minute', '60'],
        ['2 minutes', '120'],
        ['3 minutes', '180'],
        ['5 minutes', '300'],
        ['10 minutes', '600'],
        ['15 minutes', '900'],
        ['30 minutes', '1800'],
        ['1 hour', '3600'],
        ['2 hours', '7200'],
        ['4 hours', '14400'],
        ['8 hours', '28800'],
        ['1 day', '86400'],
    ],
    mainBlocks   : ['trade', 'before_purchase', 'after_purchase', 'during_purchase'],
    durationTypes: {
        RISEFALL: [
            [translate('Ticks'), 't'],
            [translate('Seconds'), 's'],
            [translate('Minutes'), 'm'],
            [translate('Hours'), 'h'],
        ],
        HIGHERLOWER: [
            [translate('Ticks'), 't'],
            [translate('Seconds'), 's'],
            [translate('Minutes'), 'm'],
            [translate('Hours'), 'h'],
        ],
        TOUCHNOTOUCH  : [[translate('Ticks'), 't'], [translate('Minutes'), 'm'], [translate('Hours'), 'h']],
        ENDSINOUT     : [[translate('Minutes'), 'm'], [translate('Hours'), 'h']],
        STAYSINOUT    : [[translate('Minutes'), 'm'], [translate('Hours'), 'h']],
        ASIANS        : [[translate('Ticks'), 't']],
        MATCHESDIFFERS: [[translate('Ticks'), 't']],
        EVENODD       : [[translate('Ticks'), 't']],
        OVERUNDER     : [[translate('Ticks'), 't']],
    },
    hasPrediction         : ['MATCHESDIFFERS', 'OVERUNDER'],
    hasBarrierOffset      : ['HIGHERLOWER', 'TOUCHNOTOUCH', 'ENDSINOUT', 'STAYSINOUT'],
    hasSecondBarrierOffset: ['ENDSINOUT', 'STAYSINOUT'],
    conditionsCategory    : {
        callput     : ['risefall', 'higherlower'],
        touchnotouch: ['touchnotouch'],
        endsinout   : ['endsinout'],
        staysinout  : ['staysinout'],
        asian       : ['asians'],
        digits      : ['matchesdiffers', 'evenodd', 'overunder'],
    },
    conditionsCategoryName: {
        callput     : translate('Up/Down'),
        asian       : translate('Asians'),
        digits      : translate('Digits'),
        touchnotouch: translate('Touch/No Touch'),
        endsinout   : translate('Ends In/Out'),
        staysinout  : translate('Stays In/Goes Out'),
    },
    conditions: [
        'risefall',
        'higherlower',
        'touchnotouch',
        'endsinout',
        'staysinout',
        'asians',
        'matchesdiffers',
        'evenodd',
        'overunder',
    ],
    scopeNames: {
        before_purchase: translate('Before Purchase'),
        during_purchase: translate('During Purchase'),
        after_purchase : translate('After Purchase'),
        tick_analysis  : translate('Tick Analysis'),
        timeout        : translate('Run After n Seconds'),
        interval       : translate('Run Every n Seconds'),
    },
    bbResult  : [[translate('upper'), '1'], [translate('middle'), '0'], [translate('lower'), '2']],
    macdFields: [[translate('Histogram'), '0'], [translate('MACD'), '1'], [translate('Signal'), '2']],
};

export async function updateConfigCurrencies() {
    const api = generateLiveApiInstance();
    try {
        const response = await api.getPayoutCurrencies();
        config.lists.CURRENCY = response.payout_currencies.map(c => [c, c]);
    } catch (e) {
        config.lists.CURRENCY = ['USD', 'EUR', 'GBP', 'AUD', ...CRYPTO_CURRENCIES].map(c => [c, c]);
    }
}

export default config;
<|endoftext|>


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

    angular.module('sslv2App')
        .controller('ProgramCtrl', ProgramCtrl);

    ProgramCtrl.$inject = ['$state', 'ProgramService','$filter','$sce','$confirm'];

    function ProgramCtrl($state, ProgramService,$filter,$sce,$confirm) {

        var vm = this;
        vm.show_program = false;
        vm.message = "";
        vm.deleteProgram = deleteProgram;
        ProgramService.getAll()
            .then(function(response){
                var data = _.get(response,"data","");
                if(data.success === true && data.total > 0){
                    var listProgram = "";
                    listProgram = _.map(data.data,function(value){
                        value.cohorts = _.map(value.cohorts,function(c){
                            return $sce.trustAsHtml(''+c+'');
                        }).join(' ');
                        return value;
                    });
                    vm.programs = $filter('orderBy')(listProgram,'name');
                    vm.show_program = true;
                }else{
                    vm.show_program = false;
                }
            },function(error){

            });

        function deleteProgram(id,index){
            $confirm({
              title: 'Delete Program',
              text:'Are you sure you want to delete this record?'
            })
                .then(function(){
                    ProgramService.deleteProgram(id)
                        .then(function(response){
                            if(response.data.success === true){
                                vm.programs.splice(index,1);
                            }
                        },function(error){
                            console.log(error);
                        })
                });
        }
    }

})();
<commit_msg>show empty programs list if no programs<commit_after>(function() {
    'use strict';

    angular.module('sslv2App')
        .controller('ProgramCtrl', ProgramCtrl);

    ProgramCtrl.$inject = ['$state', 'ProgramService','$filter','$sce','$confirm'];

    function ProgramCtrl($state, ProgramService,$filter,$sce,$confirm) {

        var vm = this;
        vm.show_program = false;
        vm.message = "";
        vm.deleteProgram = deleteProgram;
        ProgramService.getAll()
            .then(function(response){
                var data = _.get(response,"data","");
                if(data.success === true && data.total > 0){
                    var listProgram = "";
                    listProgram = _.map(data.data,function(value){
                        value.cohorts = _.map(value.cohorts,function(c){
                            return $sce.trustAsHtml(''+c+'');
                        }).join(' ');
                        return value;
                    });
                    vm.programs = $filter('orderBy')(listProgram,'name');
                }
                vm.show_program = true;
            },function(error){

            });

        function deleteProgram(id,index){
            $confirm({
              title: 'Delete Program',
              text:'Are you sure you want to delete this record?'
            })
                .then(function(){
                    ProgramService.deleteProgram(id)
                        .then(function(response){
                            if(response.data.success === true){
                                vm.programs.splice(index,1);
                            }
                        },function(error){
                            console.log(error);
                        })
                });
        }
    }

})();
<|endoftext|>


<commit_before>var util = require("./util");

var merge = module.exports = {
  longestCommonSubsequence: util.memoize(function(a, b) {
    if (a.length === 0 || b.length === 0) {
      return [];
    } else {
      var aRemaining = a.slice(0, -1);
      var bRemaining = b.slice(0, -1);

      if (a[a.length - 1] === b[b.length - 1]) {
        return merge.longestCommonSubsequence(aRemaining, bRemaining).concat(a[a.length - 1]);
      } else {
        var aLcs = merge.longestCommonSubsequence(a, bRemaining);
        var bLcs = merge.longestCommonSubsequence(aRemaining, b);
        return aLcs.length > bLcs.length ? aLcs : bLcs;
      }
    }
  }),

  align: function(a, b, lcs, out) {
    if (lcs === undefined) {
      lcs = merge.longestCommonSubsequence(a, b);
      return merge.align(a, b, lcs, { a: [], b: [] });
    }

    if (a.length === 0 && b.length === 0) {
      return out;
    } else if (a[0] === lcs[0] && b[0] === lcs[0]) {
      out.a.push(lcs[0]);
      out.b.push(lcs[0]);
      return merge.align(a.slice(1), b.slice(1), lcs.slice(1), out);
    } else if (a[0] !== lcs[0] && b[0] !== lcs[0]) {
      out.a.push(a[0]);
      out.b.push(b[0]);
      return merge.align(a.slice(1), b.slice(1), lcs, out);
    } else if (a[0] === lcs[0]) {
      out.a.push(undefined);
      out.b.push(b[0]);
      return merge.align(a, b.slice(1), lcs, out);
    } else if (b[0] === lcs[0]) {
      out.a.push(a[0]);
      out.b.push(undefined);
      return merge.align(a.slice(1), b, lcs, out);
    }
  }
};
<commit_msg>Add merge.readCommonAncestor<commit_after>var objects = require("./objects");
var util = require("./util");

var merge = module.exports = {
  longestCommonSubsequence: util.memoize(function(a, b) {
    if (a.length === 0 || b.length === 0) {
      return [];
    } else {
      var aRemaining = a.slice(0, -1);
      var bRemaining = b.slice(0, -1);

      if (a[a.length - 1] === b[b.length - 1]) {
        return merge.longestCommonSubsequence(aRemaining, bRemaining).concat(a[a.length - 1]);
      } else {
        var aLcs = merge.longestCommonSubsequence(a, bRemaining);
        var bLcs = merge.longestCommonSubsequence(aRemaining, b);
        return aLcs.length > bLcs.length ? aLcs : bLcs;
      }
    }
  }),

  align: function(a, b, lcs, out) {
    if (lcs === undefined) {
      lcs = merge.longestCommonSubsequence(a, b);
      return merge.align(a, b, lcs, { a: [], b: [] });
    }

    if (a.length === 0 && b.length === 0) {
      return out;
    } else if (a[0] === lcs[0] && b[0] === lcs[0]) {
      out.a.push(lcs[0]);
      out.b.push(lcs[0]);
      return merge.align(a.slice(1), b.slice(1), lcs.slice(1), out);
    } else if (a[0] !== lcs[0] && b[0] !== lcs[0]) {
      out.a.push(a[0]);
      out.b.push(b[0]);
      return merge.align(a.slice(1), b.slice(1), lcs, out);
    } else if (a[0] === lcs[0]) {
      out.a.push(undefined);
      out.b.push(b[0]);
      return merge.align(a, b.slice(1), lcs, out);
    } else if (b[0] === lcs[0]) {
      out.a.push(a[0]);
      out.b.push(undefined);
      return merge.align(a.slice(1), b, lcs, out);
    }
  },

  readCommonAncestor: function(aHash, bHash) {
    var aAncestors = objects.readAncestors(aHash);
    var bAncestors = objects.readAncestors(bHash);
    return aAncestors
      .filter(function(a) { return bAncestors.indexOf(a) !== -1; })
      .sort()[0]; // sort to make sure answer is commutative
  }
};
<|endoftext|>


<commit_before>'use strict';

/**
 * @ngdoc directive
 * @name globiProtoApp.directive:columnNetworkVis
 * @description
 * # columnNetworkVis
 */
angular.module('globiProtoApp')
  .directive('columnNetworkVis', function (columnGraphValues, graphService) {
    return {
      restrict: 'E',
      scope: {
        val: '='
      },
      link: function postLink(scope, element) {

        // Color scale
        var color = d3.scale.category10();

        // Init the vis
        var svg = d3.select(element[0]).append('svg')
          .attr('width', columnGraphValues.width)
          .attr('height', columnGraphValues.height);

        // Keep reference to nodes and links to support updates
        var nodes = [];
        var links = [];

        // Implement D3 general updating pattern
        var update = function() {

          // Node groups (circle + label)
          var node = svg.selectAll('.node').data(nodes);
          var nodeEnter = node.enter().append('g')
            .attr('class', 'node');

          // Node circles (initial positions start them at their respective sources, then later will transition)
          var circles = nodeEnter.append('circle')
            .attr('cx', function(d) { return d.initialXPos; })
            .attr('cy', function(d) { return d.initialYPos; })
            .attr('r', 1)
            .style('fill', function (d) { return color(d.group); })
            .on('click', function(item) {
              item.circleColor = d3.select(this).attr('style').split('fill: ')[1];
              scope.$emit('nodeClicked', item);
            });

          // Now transition all the circles to their new positions
          // Pass a function to delay to stagger the transitions,
          // (otherwise circles all bounce at once and look like they're attached)
          circles.transition()
            .delay(function(d, i) {
              // first circle will be not delayed (0*100=0)
              // second circle will be delayed by 100ms (1*100=100)
              // third circle will be delayed by 2*100=200ms
              // etc.
              return i * 50;
            })
            .duration(300)
            .ease('linear')
            .attr('r', 10)
            .attr('cx', function(d) { return d.xPos; })
            .attr('cy', function(d) { return d.yPos; });

          // Node labels
          nodeEnter.append('text')
            .attr('dx', function(d) {return d.xPos + 10;})
            .attr('dy', function(d) {return d.yPos + 5;})
            .text(function(d) { return d.name; })
            .style('stroke', function(d) {return color(d.group);});

          node.exit().remove();

          // Links between nodes
          var link = svg.selectAll('.link').data(links);
          link.enter().insert('line')
            .attr('class', 'link')
            .attr('x1', function(d) { return nodes[d.source].xPos; })
            .attr('y1', function(d) { return nodes[d.source].yPos; })
            .attr('x2', function(d) { return nodes[d.target].xPos; })
            .attr('y2', function(d) { return nodes[d.target].yPos; })
            .style('stroke-width', '2');

          link.exit().remove();

        };//update

        // Watch for new graph data in scope
        scope.$watch('val', function(newVal) {

          // Nothing to do if no new data available
          if (!newVal) {return; }

          // Append new graph data and update vis
          nodes.push.apply(nodes, newVal.nodes);
          links.push.apply(links, newVal.links);
          update();

          // Redraw ALL the text labels based on path
          d3.selectAll('text')
            .style('stroke', function(d) {
              if (graphService.isNodeInPath(d.name) || graphService.isNodeTargetOfPathTip(d.name)) {
                return color(d.group);
              } else {
                return '#b3b1b1';
              }
            })
            .style('font-size', function(d) {
              if (!graphService.isNodeInPath(d.name) && !graphService.isNodeTargetOfPathTip(d.name)) {
                return '10px';
              }
            });

        });//scope.$watch

      }//link
    };//return
  });//directive
<commit_msg>more transition effects<commit_after>'use strict';

/**
 * @ngdoc directive
 * @name globiProtoApp.directive:columnNetworkVis
 * @description
 * # columnNetworkVis
 */
angular.module('globiProtoApp')
  .directive('columnNetworkVis', function (columnGraphValues, graphService) {
    return {
      restrict: 'E',
      scope: {
        val: '='
      },
      link: function postLink(scope, element) {

        // Color scale
        var color = d3.scale.category10();

        // Init the vis
        var svg = d3.select(element[0]).append('svg')
          .attr('width', columnGraphValues.width)
          .attr('height', columnGraphValues.height);

        // Keep reference to nodes and links to support updates
        var nodes = [];
        var links = [];

        // Implement D3 general updating pattern
        var update = function() {

          // Node groups (circle + label)
          var node = svg.selectAll('.node').data(nodes);
          var nodeEnter = node.enter().append('g')
            .attr('class', 'node');

          // Node circles (initial positions start them at their respective sources, then later will transition)
          var circles = nodeEnter.append('circle')
            .attr('cx', function(d) { return d.initialXPos; })
            .attr('cy', function(d) { return d.initialYPos; })
            .attr('r', 1)
            .style('fill', function (d) { return color(d.group); })
            .on('click', function(item) {
              item.circleColor = d3.select(this).attr('style').split('fill: ')[1];
              scope.$emit('nodeClicked', item);
            });

          // Transition circles to their new positions
          circles.transition()
            .delay(function(d, i) {
              return i * 10;
            })
            .duration(300)
            .ease('linear')
            .attr('r', 10)
            .attr('cx', function(d) { return d.xPos; })
            .attr('cy', function(d) { return d.yPos; });

          // Node labels
          nodeEnter.append('text')
            .attr('dx', function(d) {return d.xPos + 10;})
            .attr('dy', function(d) {return d.yPos + 5;})
            .text(function(d) { return d.name; })
            .style('stroke', function(d) {return color(d.group);});

          node.exit().remove();

          // Links between nodes (initial positions make x2/y2 points the same as x1/y1)
          var link = svg.selectAll('.link').data(links);
          var lineLinks = link.enter().insert('line')
            .attr('class', 'link')
            .attr('x1', function(d) { return nodes[d.source].xPos; })
            .attr('y1', function(d) { return nodes[d.source].yPos; })
            .attr('x2', function(d) { return nodes[d.source].xPos; })
            .attr('y2', function(d) { return nodes[d.source].yPos; })
            .style('stroke-width', '2');

          // Transition line links end points to their new positions
          lineLinks.transition()
            .delay(function(d, i) {
              return i * 10;
            })
            .duration(300)
            .ease('linear')
            .attr('x2', function(d) { return nodes[d.target].xPos; })
            .attr('y2', function(d) { return nodes[d.target].yPos; });

          link.exit().remove();

        };//update

        // Watch for new graph data in scope
        scope.$watch('val', function(newVal) {

          // Nothing to do if no new data available
          if (!newVal) {return; }

          // Append new graph data and update vis
          nodes.push.apply(nodes, newVal.nodes);
          links.push.apply(links, newVal.links);
          update();

          // Redraw ALL the text labels based on path, de-emphasize those not on path
          d3.selectAll('text')
            .style('stroke', function(d) {
              if (graphService.isNodeInPath(d.name) || graphService.isNodeTargetOfPathTip(d.name)) {
                return color(d.group);
              } else {
                return '#b3b1b1';
              }
            })
            .style('font-size', function(d) {
              if (!graphService.isNodeInPath(d.name) && !graphService.isNodeTargetOfPathTip(d.name)) {
                return '10px';
              }
            });

        });//scope.$watch

      }//link
    };//return
  });//directive
<|endoftext|>