<commit_before>import { LessonsPageNames } from '../../constants/lessonsConstants';
import { getChannels } from 'kolibri.coreVue.vuex.getters';
import { assessmentMetaDataState } from 'kolibri.coreVue.vuex.mappers';
import { setClassState } from './main';
import { LearnerGroupResource, LessonResource, ContentNodeResource } from 'kolibri.resources';
import LessonReportResource from '../../apiResources/lessonReport';
import { ContentNodeKinds } from 'kolibri.coreVue.vuex.constants';
import { createTranslator } from 'kolibri.utils.i18n';
import { getContentNodeThumbnail } from 'kolibri.utils.contentNode';
import { handleApiError, createSnackbar } from 'kolibri.coreVue.vuex.actions';
import { error as logError } from 'kolibri.lib.logging';
import router from 'kolibri.coreVue.router';
import { lessonSummaryLink } from '../../views/lessons/lessonsRouterUtils';

const translator = createTranslator('lessonsPageTitles', {
  lessons: 'Lessons',
  selectResources: 'Select resources',
});

// Show the Lessons Root Page, where all the Lessons are listed for a given Classroom
export function showLessonsRootPage(store, classId) {
  store.dispatch('CORE_SET_PAGE_LOADING', true);
  store.dispatch('SET_PAGE_STATE', {
    lessons: [],
    learnerGroups: [],
  });
  const loadRequirements = [
    // Fetch learner groups for the New Lesson Modal
    LearnerGroupResource.getCollection({ parent: classId }).fetch(),
    refreshClassLessons(store, classId),
    setClassState(store, classId),
  ];
  return Promise.all(loadRequirements).then(
    ([learnerGroups]) => {
      store.dispatch('SET_LEARNER_GROUPS', learnerGroups);
      store.dispatch('SET_PAGE_NAME', LessonsPageNames.ROOT);
      store.dispatch('CORE_SET_TITLE', translator.$tr('lessons'));
      store.dispatch('CORE_SET_PAGE_LOADING', false);
    },
    error => {
      handleApiError(store, error);
      store.dispatch('CORE_SET_PAGE_LOADING', false);
    }
  );
}

export function refreshClassLessons(store, classId) {
  return LessonResource.getCollection({ collection: classId })
    .fetch({}, true)
    ._promise.then(lessons => {
      store.dispatch('SET_CLASS_LESSONS', lessons);
      // resolve lessons in case it's needed
      return lessons;
    })
    .catch(error => {
      return handleApiError(store, error);
    });
}

export function updateCurrentLesson(store, lessonId) {
  const currentLessonId = lessonId || store.state.pageState.lessonId;

  return (
    LessonResource.getModel(currentLessonId)
      .fetch()
      // is lesson set appropriately here?
      .then(
        lesson => {
          store.dispatch('SET_CURRENT_LESSON', lesson);
          // resolve lesson in case it's needed
          return lesson;
        },
        error => {
          return handleApiError(store, error);
        }
      )
  );
}

export function showLessonSummaryPage(store, classId, lessonId) {
  store.dispatch('CORE_SET_PAGE_LOADING', true);
  store.dispatch('SET_PAGE_STATE', {
    currentLesson: {},
    lessonReport: {},
    workingResources: [],
    resourceCache: store.state.pageState.resourceCache || {},
    lessonsModalSet: null,
  });

  const loadRequirements = [
    updateCurrentLesson(store, lessonId),
    LearnerGroupResource.getCollection({ parent: classId }).fetch(),
    LessonReportResource.getModel(lessonId).fetch({}, true),
    setClassState(store, classId),
  ];

  Promise.all(loadRequirements)
    .then(([currentLesson, learnerGroups, lessonReport]) => {
      // TODO state mapper
      const resourceIds = currentLesson.resources.map(resourceObj => resourceObj.contentnode_id);

      return getResourceCache(store, resourceIds).then(() => {
        store.dispatch('SET_WORKING_RESOURCES', resourceIds);
        store.dispatch('SET_LEARNER_GROUPS', learnerGroups);
        store.dispatch('SET_LESSON_REPORT', lessonReport);
        store.dispatch('CORE_SET_PAGE_LOADING', false);
        store.dispatch('SET_PAGE_NAME', LessonsPageNames.SUMMARY);
        store.dispatch('CORE_SET_TITLE', currentLesson.title);
      });
    })
    .catch(error => {
      store.dispatch('CORE_SET_PAGE_LOADING', false);
      return handleApiError(store, error);
    });
}

/* eslint-disable no-unused-vars */
function showResourceSelectionPage(
  store,
  classId,
  lessonId,
  contentList,
  pageName,
  ancestors = []
) {
  const pendingSelections = store.state.pageState.workingResources || [];
  const cache = store.state.pageState.resourceCache || {};
  const pageState = {
    currentLesson: {},
    contentList: [],
    ancestors: [],
    toolbarRoute: {},
    // contains all selections, including those that haven't been committed to server
    workingResources: pendingSelections,
    resourceCache: cache,
  };
  store.dispatch('CORE_SET_PAGE_LOADING', true);
  store.dispatch('SET_PAGE_STATE', pageState);

  const loadRequirements = [updateCurrentLesson(store, lessonId), setClassState(store, classId)];
  return Promise.all(loadRequirements).then(
    ([currentLesson]) => {
      // TODO make a state mapper
      // contains selections that were commited to server prior to opening this page
      if (!pendingSelections.length) {
        const preselectedResources = currentLesson.resources.map(
          resourceObj => resourceObj.contentnode_id
        );
        store.dispatch('SET_WORKING_RESOURCES', preselectedResources);
      }

      if (ancestors.length) {
        store.dispatch('SET_ANCESTORS', ancestors);
      }

      const ancestorCounts = {};

      const getResourceAncestors = store.state.pageState.workingResources.map(resourceId =>
        ContentNodeResource.fetchAncestors(resourceId)
      );

      return Promise.all(getResourceAncestors).then(
        // there has to be a better way
        resourceAncestors => {
          resourceAncestors.forEach(ancestorArray =>
            ancestorArray.forEach(ancestor => {
              if (ancestorCounts[ancestor.pk]) {
                ancestorCounts[ancestor.pk]++;
              } else {
                ancestorCounts[ancestor.pk] = 1;
              }
            })
          );
          store.dispatch('SET_ANCESTOR_COUNTS', ancestorCounts);
          // carry pendingSelections over from other interactions in this modal
          store.dispatch('SET_CONTENT_LIST', contentList);
          store.dispatch('SET_PAGE_NAME', pageName);
          store.dispatch('SET_TOOLBAR_ROUTE', { name: LessonsPageNames.SUMMARY });
          store.dispatch('CORE_SET_TITLE', translator.$tr('selectResources'));
          store.dispatch('CORE_SET_PAGE_LOADING', false);
        }
      );
    },
    error => {
      store.dispatch('CORE_SET_PAGE_LOADING', false);
      return handleApiError(store, error);
    }
  );
}

export function showLessonResourceSelectionRootPage(store, classId, lessonId) {
  store.dispatch('CORE_SET_PAGE_LOADING', true);
  const channelContentList = getChannels(store.state).map(channel => {
    return {
      id: channel.root_id,
      description: channel.description,
      title: channel.title,
      thumbnail: channel.thumbnail,
      kind: ContentNodeKinds.CHANNEL,
    };
  });

  return showResourceSelectionPage(
    store,
    classId,
    lessonId,
    channelContentList,
    LessonsPageNames.SELECTION_ROOT
  );
}

export function showLessonResourceSelectionTopicPage(store, classId, lessonId, topicId) {
  // IDEA should probably have both selection pages set loading themselves
  store.dispatch('CORE_SET_PAGE_LOADING', true);
  const loadRequirements = [
    ContentNodeResource.getModel(topicId).fetch(),
    ContentNodeResource.getCollection({ parent: topicId }).fetch(),
    ContentNodeResource.fetchAncestors(topicId),
  ];

  return Promise.all(loadRequirements).then(
    ([topicNode, childNodes, ancestors]) => {
      const topicAncestors = [...ancestors, topicNode];
      // TODO state mapper
      const topicContentList = childNodes.map(node => {
        return {
          id: node.pk,
          content_id: node.content_id,
          channel_id: node.channel_id,
          description: node.description,
          title: node.title,
          thumbnail: getContentNodeThumbnail(node),
          kind: node.kind,
        };
      });

      return showResourceSelectionPage(
        store,
        classId,
        lessonId,
        topicContentList,
        LessonsPageNames.SELECTION,
        topicAncestors
      );
    },
    error => {
      store.dispatch('CORE_SET_PAGE_LOADING', false);
      return handleApiError(store, error);
    }
  );
}

function getResourceCache(store, resourceIds) {
  const { resourceCache } = store.state.pageState;
  const nonCachedResourceIds = [];

  if (resourceCache) {
    resourceIds.forEach(id => {
      if (!resourceCache[id]) {
        nonCachedResourceIds.push(id);
      }
    });
  }

  if (nonCachedResourceIds.length) {
    return ContentNodeResource.getCollection({
      ids: nonCachedResourceIds,
    })
      .fetch()
      ._promise.then(contentNodes => {
        contentNodes.forEach(contentNode => store.dispatch('ADD_TO_RESOURCE_CACHE', contentNode));
        return resourceCache;
      });
  } else {
    return Promise.resolve(resourceCache);
  }
}

export function saveLessonResources(store, lessonId, resourceIds) {
  return getResourceCache(store, resourceIds).then(resourceCache => {
    const resources = resourceIds.map(resourceId => {
      const node = resourceCache[resourceId];
      return {
        contentnode_id: resourceId,
        channel_id: node.channel_id,
        content_id: node.content_id,
      };
    });

    return LessonResource.getModel(lessonId).save({ resources });
  });
}

export function showLessonResourceContentPreview(store, classId, lessonId, contentId) {
  store.dispatch('CORE_SET_PAGE_LOADING', true);
  _prepLessonContentPreview(store, classId, lessonId, contentId).then(() => {
    store.dispatch('SET_TOOLBAR_ROUTE', {
      name: LessonsPageNames.RESOURCE_USER_SUMMARY,
    });
    store.dispatch('CORE_SET_PAGE_LOADING', false);
  });
}

export function showLessonSelectionContentPreview(store, classId, lessonId, contentId) {
  store.dispatch('CORE_SET_PAGE_LOADING', true);
  const pendingSelections = store.state.pageState.workingResources || [];
  return Promise.all([
    _prepLessonContentPreview(store, classId, lessonId, contentId),
    updateCurrentLesson(store, lessonId),
  ])
    .then(([contentNode, lesson]) => {
      // TODO state mapper
      const preselectedResources = lesson.resources.map(resourceObj => resourceObj.contentnode_id);
      store.dispatch('SET_TOOLBAR_ROUTE', {
        name: LessonsPageNames.SELECTION,
        params: {
          topicId: contentNode.parent,
        },
      });
      store.dispatch(
        'SET_WORKING_RESOURCES',
        pendingSelections.length ? pendingSelections : preselectedResources
      );
      store.dispatch('CORE_SET_PAGE_LOADING', false);
    })
    .catch(error => {
      store.dispatch('CORE_SET_PAGE_LOADING', false);
      return handleApiError(store, error);
    });
}

function _prepLessonContentPreview(store, classId, lessonId, contentId) {
  const cache = store.state.pageState.resourceCache || {};
  const pageState = {
    currentContentNode: {},
    toolbarRoute: {},
    // only exist if exercises
    workingResources: null,
    resourceCache: cache,
    questions: null,
    completionData: null,
  };
  return ContentNodeResource.getModel(contentId)
    .fetch()
    .then(
      contentNode => {
        // set up intial pageState
        const contentMetadata = assessmentMetaDataState(contentNode);
        pageState.currentContentNode = contentNode;
        pageState.questions = contentMetadata.assessmentIds;
        pageState.completionData = contentMetadata.masteryModel;
        store.dispatch('SET_PAGE_STATE', pageState);
        store.dispatch('CORE_SET_TITLE', contentNode.title);
        store.dispatch('SET_PAGE_NAME', LessonsPageNames.CONTENT_PREVIEW);
      },
      error => {
        return handleApiError(store, error);
      }
    );
}

export function setLessonsModal(store, modalName) {
  store.dispatch('SET_LESSONS_MODAL', modalName);
}

export function updateLessonStatus(store, lessonId, isActive) {
  LessonResource.getModel(lessonId)
    .save({
      is_active: isActive,
    })
    ._promise.then(lesson => {
      store.dispatch('SET_CURRENT_LESSON', lesson);
      setLessonsModal(store, null);

      const trs = createTranslator('updateLessonStatus', {
        lessonIsNowActive: 'Lesson is now active',
        lessonIsNowInactive: 'Lesson is now inactive',
      });

      createSnackbar(store, {
        text: isActive ? trs.$tr('lessonIsNowActive') : trs.$tr('lessonIsNowInactive'),
        autoDismiss: true,
      });
    })
    .catch(err => {
      // TODO handle error properly
      handleApiError(store, err);
      logError(err);
    });
}

export function deleteLesson(store, lessonId, classId) {
  LessonResource.getModel(lessonId)
    .delete()
    ._promise.then(() => refreshClassLessons(store, classId))
    .then(() => {
      router.replace({
        name: LessonsPageNames.ROOT,
        params: {
          classId,
          lessonId,
        },
      });
      createSnackbar(store, {
        text: createTranslator('lessonDeletedSnackbar', {
          lessonDeleted: 'Lesson deleted',
        }).$tr('lessonDeleted'),
        autoDismiss: true,
      });
    })
    .catch(error => {
      // TODO handle error inside the current page
      handleApiError(store, error);
      logError(error);
    });
}

export function copyLesson(store, payload, classroomName) {
  LessonResource.createModel(payload)
    .save()
    ._promise.then(() => {
      setLessonsModal(store, null);
      createSnackbar(store, {
        text: createTranslator('lessonCopiedSnackbar', {
          copiedLessonTo: `Copied lesson to '{classroomName}'`,
        }).$tr('copiedLessonTo', { classroomName }),
        autoDismiss: true,
      });
    })
    .catch(error => {
      handleApiError(store, error);
      logError(error);
    });
}

export function updateLesson(store, lessonId, payload) {
  return new Promise((resolve, reject) => {
    LessonResource.getModel(lessonId)
      .save(payload)
      .then(updatedLesson => {
        setLessonsModal(store, null);
        createSnackbar(store, {
          text: createTranslator('lessonUpdatedSnackbar', {
            changesToLessonSaved: 'Changes to lesson saved',
          }).$tr('changesToLessonSaved'),
          autoDismiss: true,
        });
        updateCurrentLesson(store, lessonId);
        resolve();
      })
      .catch(error => {
        reject();
      });
  });
}

export function createLesson(store, classId, payload) {
  return new Promise((resolve, reject) => {
    return LessonResource.createModel({
      ...payload,
      resources: [],
      collection: classId,
    })
      .save()
      .then(newLesson => {
        setLessonsModal(store, null);
        router.push(lessonSummaryLink({ classId: classId, lessonId: newLesson.id }));
        createSnackbar(store, {
          text: createTranslator('lessonCreatedSnackbar', {
            newLessonCreated: 'New lesson created',
          }).$tr('newLessonCreated'),
          autoDismiss: true,
        });
        resolve();
      })
      .catch(() => {
        reject();
      });
  });
}
<commit_msg>Fixing redbox on content preview page. Fixing broken "back" link on preview page.<commit_after>import { LessonsPageNames } from '../../constants/lessonsConstants';
import { getChannels } from 'kolibri.coreVue.vuex.getters';
import { assessmentMetaDataState } from 'kolibri.coreVue.vuex.mappers';
import { setClassState } from './main';
import { LearnerGroupResource, LessonResource, ContentNodeResource } from 'kolibri.resources';
import LessonReportResource from '../../apiResources/lessonReport';
import { ContentNodeKinds } from 'kolibri.coreVue.vuex.constants';
import { createTranslator } from 'kolibri.utils.i18n';
import { getContentNodeThumbnail } from 'kolibri.utils.contentNode';
import { handleApiError, createSnackbar } from 'kolibri.coreVue.vuex.actions';
import { error as logError } from 'kolibri.lib.logging';
import router from 'kolibri.coreVue.router';
import { lessonSummaryLink } from '../../views/lessons/lessonsRouterUtils';

const translator = createTranslator('lessonsPageTitles', {
  lessons: 'Lessons',
  selectResources: 'Select resources',
});

// Show the Lessons Root Page, where all the Lessons are listed for a given Classroom
export function showLessonsRootPage(store, classId) {
  store.dispatch('CORE_SET_PAGE_LOADING', true);
  store.dispatch('SET_PAGE_STATE', {
    lessons: [],
    learnerGroups: [],
  });
  const loadRequirements = [
    // Fetch learner groups for the New Lesson Modal
    LearnerGroupResource.getCollection({ parent: classId }).fetch(),
    refreshClassLessons(store, classId),
    setClassState(store, classId),
  ];
  return Promise.all(loadRequirements).then(
    ([learnerGroups]) => {
      store.dispatch('SET_LEARNER_GROUPS', learnerGroups);
      store.dispatch('SET_PAGE_NAME', LessonsPageNames.ROOT);
      store.dispatch('CORE_SET_TITLE', translator.$tr('lessons'));
      store.dispatch('CORE_SET_PAGE_LOADING', false);
    },
    error => {
      handleApiError(store, error);
      store.dispatch('CORE_SET_PAGE_LOADING', false);
    }
  );
}

export function refreshClassLessons(store, classId) {
  return LessonResource.getCollection({ collection: classId })
    .fetch({}, true)
    ._promise.then(lessons => {
      store.dispatch('SET_CLASS_LESSONS', lessons);
      // resolve lessons in case it's needed
      return lessons;
    })
    .catch(error => {
      return handleApiError(store, error);
    });
}

export function updateCurrentLesson(store, lessonId) {
  const currentLessonId = lessonId || store.state.pageState.lessonId;

  return (
    LessonResource.getModel(currentLessonId)
      .fetch()
      // is lesson set appropriately here?
      .then(
        lesson => {
          store.dispatch('SET_CURRENT_LESSON', lesson);
          // resolve lesson in case it's needed
          return lesson;
        },
        error => {
          return handleApiError(store, error);
        }
      )
  );
}

export function showLessonSummaryPage(store, classId, lessonId) {
  store.dispatch('CORE_SET_PAGE_LOADING', true);
  store.dispatch('SET_PAGE_STATE', {
    currentLesson: {},
    lessonReport: {},
    workingResources: [],
    resourceCache: store.state.pageState.resourceCache || {},
    lessonsModalSet: null,
  });

  const loadRequirements = [
    updateCurrentLesson(store, lessonId),
    LearnerGroupResource.getCollection({ parent: classId }).fetch(),
    LessonReportResource.getModel(lessonId).fetch({}, true),
    setClassState(store, classId),
  ];

  Promise.all(loadRequirements)
    .then(([currentLesson, learnerGroups, lessonReport]) => {
      // TODO state mapper
      const resourceIds = currentLesson.resources.map(resourceObj => resourceObj.contentnode_id);

      return getResourceCache(store, resourceIds).then(() => {
        store.dispatch('SET_WORKING_RESOURCES', resourceIds);
        store.dispatch('SET_LEARNER_GROUPS', learnerGroups);
        store.dispatch('SET_LESSON_REPORT', lessonReport);
        store.dispatch('CORE_SET_PAGE_LOADING', false);
        store.dispatch('SET_PAGE_NAME', LessonsPageNames.SUMMARY);
        store.dispatch('CORE_SET_TITLE', currentLesson.title);
      });
    })
    .catch(error => {
      store.dispatch('CORE_SET_PAGE_LOADING', false);
      return handleApiError(store, error);
    });
}

/* eslint-disable no-unused-vars */
function showResourceSelectionPage(
  store,
  classId,
  lessonId,
  contentList,
  pageName,
  ancestors = []
) {
  const pendingSelections = store.state.pageState.workingResources || [];
  const cache = store.state.pageState.resourceCache || {};
  const pageState = {
    currentLesson: {},
    contentList: [],
    ancestors: [],
    toolbarRoute: {},
    // contains all selections, including those that haven't been committed to server
    workingResources: pendingSelections,
    resourceCache: cache,
  };
  store.dispatch('CORE_SET_PAGE_LOADING', true);
  store.dispatch('SET_PAGE_STATE', pageState);

  const loadRequirements = [updateCurrentLesson(store, lessonId), setClassState(store, classId)];
  return Promise.all(loadRequirements).then(
    ([currentLesson]) => {
      // TODO make a state mapper
      // contains selections that were commited to server prior to opening this page
      if (!pendingSelections.length) {
        const preselectedResources = currentLesson.resources.map(
          resourceObj => resourceObj.contentnode_id
        );
        store.dispatch('SET_WORKING_RESOURCES', preselectedResources);
      }

      if (ancestors.length) {
        store.dispatch('SET_ANCESTORS', ancestors);
      }

      const ancestorCounts = {};

      const getResourceAncestors = store.state.pageState.workingResources.map(resourceId =>
        ContentNodeResource.fetchAncestors(resourceId)
      );

      return Promise.all(getResourceAncestors).then(
        // there has to be a better way
        resourceAncestors => {
          resourceAncestors.forEach(ancestorArray =>
            ancestorArray.forEach(ancestor => {
              if (ancestorCounts[ancestor.pk]) {
                ancestorCounts[ancestor.pk]++;
              } else {
                ancestorCounts[ancestor.pk] = 1;
              }
            })
          );
          store.dispatch('SET_ANCESTOR_COUNTS', ancestorCounts);
          // carry pendingSelections over from other interactions in this modal
          store.dispatch('SET_CONTENT_LIST', contentList);
          store.dispatch('SET_PAGE_NAME', pageName);
          store.dispatch('SET_TOOLBAR_ROUTE', { name: LessonsPageNames.SUMMARY });
          store.dispatch('CORE_SET_TITLE', translator.$tr('selectResources'));
          store.dispatch('CORE_SET_PAGE_LOADING', false);
        }
      );
    },
    error => {
      store.dispatch('CORE_SET_PAGE_LOADING', false);
      return handleApiError(store, error);
    }
  );
}

export function showLessonResourceSelectionRootPage(store, classId, lessonId) {
  store.dispatch('CORE_SET_PAGE_LOADING', true);
  const channelContentList = getChannels(store.state).map(channel => {
    return {
      id: channel.root_id,
      description: channel.description,
      title: channel.title,
      thumbnail: channel.thumbnail,
      kind: ContentNodeKinds.CHANNEL,
    };
  });

  return showResourceSelectionPage(
    store,
    classId,
    lessonId,
    channelContentList,
    LessonsPageNames.SELECTION_ROOT
  );
}

export function showLessonResourceSelectionTopicPage(store, classId, lessonId, topicId) {
  // IDEA should probably have both selection pages set loading themselves
  store.dispatch('CORE_SET_PAGE_LOADING', true);
  const loadRequirements = [
    ContentNodeResource.getModel(topicId).fetch(),
    ContentNodeResource.getCollection({ parent: topicId }).fetch(),
    ContentNodeResource.fetchAncestors(topicId),
  ];

  return Promise.all(loadRequirements).then(
    ([topicNode, childNodes, ancestors]) => {
      const topicAncestors = [...ancestors, topicNode];
      // TODO state mapper
      const topicContentList = childNodes.map(node => {
        return {
          id: node.pk,
          content_id: node.content_id,
          channel_id: node.channel_id,
          description: node.description,
          title: node.title,
          thumbnail: getContentNodeThumbnail(node),
          kind: node.kind,
        };
      });

      return showResourceSelectionPage(
        store,
        classId,
        lessonId,
        topicContentList,
        LessonsPageNames.SELECTION,
        topicAncestors
      );
    },
    error => {
      store.dispatch('CORE_SET_PAGE_LOADING', false);
      return handleApiError(store, error);
    }
  );
}

function getResourceCache(store, resourceIds) {
  const { resourceCache } = store.state.pageState;
  const nonCachedResourceIds = [];

  if (resourceCache) {
    resourceIds.forEach(id => {
      if (!resourceCache[id]) {
        nonCachedResourceIds.push(id);
      }
    });
  }

  if (nonCachedResourceIds.length) {
    return ContentNodeResource.getCollection({
      ids: nonCachedResourceIds,
    })
      .fetch()
      ._promise.then(contentNodes => {
        contentNodes.forEach(contentNode => store.dispatch('ADD_TO_RESOURCE_CACHE', contentNode));
        return resourceCache;
      });
  } else {
    return Promise.resolve(resourceCache);
  }
}

export function saveLessonResources(store, lessonId, resourceIds) {
  return getResourceCache(store, resourceIds).then(resourceCache => {
    const resources = resourceIds.map(resourceId => {
      const node = resourceCache[resourceId];
      return {
        contentnode_id: resourceId,
        channel_id: node.channel_id,
        content_id: node.content_id,
      };
    });

    return LessonResource.getModel(lessonId).save({ resources });
  });
}

export function showLessonResourceContentPreview(store, classId, lessonId, contentId) {
  store.dispatch('CORE_SET_PAGE_LOADING', true);
  _prepLessonContentPreview(store, classId, lessonId, contentId).then(() => {
    store.dispatch('SET_TOOLBAR_ROUTE', {
      name: LessonsPageNames.RESOURCE_USER_SUMMARY,
    });
    store.dispatch('CORE_SET_PAGE_LOADING', false);
  });
}

export function showLessonSelectionContentPreview(store, classId, lessonId, contentId) {
  store.dispatch('CORE_SET_PAGE_LOADING', true);
  const pendingSelections = store.state.pageState.workingResources || [];
  return Promise.all([
    _prepLessonContentPreview(store, classId, lessonId, contentId),
    updateCurrentLesson(store, lessonId),
  ])
    .then(([contentNode, lesson]) => {
      // TODO state mapper
      const preselectedResources = lesson.resources.map(resourceObj => resourceObj.contentnode_id);
      store.dispatch('SET_TOOLBAR_ROUTE', {
        name: LessonsPageNames.SELECTION,
        params: {
          topicId: contentNode.parent,
        },
      });
      store.dispatch(
        'SET_WORKING_RESOURCES',
        pendingSelections.length ? pendingSelections : preselectedResources
      );
      store.dispatch('CORE_SET_PAGE_LOADING', false);
    })
    .catch(error => {
      store.dispatch('CORE_SET_PAGE_LOADING', false);
      return handleApiError(store, error);
    });
}

function _prepLessonContentPreview(store, classId, lessonId, contentId) {
  const cache = store.state.pageState.resourceCache || {};
  const pageState = {
    currentContentNode: {},
    toolbarRoute: {},
    // only exist if exercises
    workingResources: null,
    resourceCache: cache,
    questions: null,
    completionData: null,
  };
  return ContentNodeResource.getModel(contentId)
    .fetch()
    .then(
      contentNode => {
        // set up intial pageState
        const contentMetadata = assessmentMetaDataState(contentNode);
        pageState.currentContentNode = contentNode;
        pageState.questions = contentMetadata.assessmentIds;
        pageState.completionData = contentMetadata.masteryModel;
        store.dispatch('SET_PAGE_STATE', pageState);
        store.dispatch('CORE_SET_TITLE', contentNode.title);
        store.dispatch('SET_PAGE_NAME', LessonsPageNames.CONTENT_PREVIEW);
        return contentNode;
      },
      error => {
        return handleApiError(store, error);
      }
    );
}

export function setLessonsModal(store, modalName) {
  store.dispatch('SET_LESSONS_MODAL', modalName);
}

export function updateLessonStatus(store, lessonId, isActive) {
  LessonResource.getModel(lessonId)
    .save({
      is_active: isActive,
    })
    ._promise.then(lesson => {
      store.dispatch('SET_CURRENT_LESSON', lesson);
      setLessonsModal(store, null);

      const trs = createTranslator('updateLessonStatus', {
        lessonIsNowActive: 'Lesson is now active',
        lessonIsNowInactive: 'Lesson is now inactive',
      });

      createSnackbar(store, {
        text: isActive ? trs.$tr('lessonIsNowActive') : trs.$tr('lessonIsNowInactive'),
        autoDismiss: true,
      });
    })
    .catch(err => {
      // TODO handle error properly
      handleApiError(store, err);
      logError(err);
    });
}

export function deleteLesson(store, lessonId, classId) {
  LessonResource.getModel(lessonId)
    .delete()
    ._promise.then(() => refreshClassLessons(store, classId))
    .then(() => {
      router.replace({
        name: LessonsPageNames.ROOT,
        params: {
          classId,
          lessonId,
        },
      });
      createSnackbar(store, {
        text: createTranslator('lessonDeletedSnackbar', {
          lessonDeleted: 'Lesson deleted',
        }).$tr('lessonDeleted'),
        autoDismiss: true,
      });
    })
    .catch(error => {
      // TODO handle error inside the current page
      handleApiError(store, error);
      logError(error);
    });
}

export function copyLesson(store, payload, classroomName) {
  LessonResource.createModel(payload)
    .save()
    ._promise.then(() => {
      setLessonsModal(store, null);
      createSnackbar(store, {
        text: createTranslator('lessonCopiedSnackbar', {
          copiedLessonTo: `Copied lesson to '{classroomName}'`,
        }).$tr('copiedLessonTo', { classroomName }),
        autoDismiss: true,
      });
    })
    .catch(error => {
      handleApiError(store, error);
      logError(error);
    });
}

export function updateLesson(store, lessonId, payload) {
  return new Promise((resolve, reject) => {
    LessonResource.getModel(lessonId)
      .save(payload)
      .then(updatedLesson => {
        setLessonsModal(store, null);
        createSnackbar(store, {
          text: createTranslator('lessonUpdatedSnackbar', {
            changesToLessonSaved: 'Changes to lesson saved',
          }).$tr('changesToLessonSaved'),
          autoDismiss: true,
        });
        updateCurrentLesson(store, lessonId);
        resolve();
      })
      .catch(error => {
        reject();
      });
  });
}

export function createLesson(store, classId, payload) {
  return new Promise((resolve, reject) => {
    return LessonResource.createModel({
      ...payload,
      resources: [],
      collection: classId,
    })
      .save()
      .then(newLesson => {
        setLessonsModal(store, null);
        router.push(lessonSummaryLink({ classId: classId, lessonId: newLesson.id }));
        createSnackbar(store, {
          text: createTranslator('lessonCreatedSnackbar', {
            newLessonCreated: 'New lesson created',
          }).$tr('newLessonCreated'),
          autoDismiss: true,
        });
        resolve();
      })
      .catch(() => {
        reject();
      });
  });
}
<|endoftext|>


<commit_before>var Class = require('../../utils/Class');
var BaseSound = require('../BaseSound');
//  Phaser.Sound.WebAudioSound
// TODO support webkitAudioContext implementation differences
// https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API/Porting_webkitAudioContext_code_to_standards_based_AudioContext
var WebAudioSound = new Class({
    Extends: BaseSound,
    initialize: function WebAudioSound(manager, key, config) {
        if (config === void 0) { config = {}; }
        /**
         * [description]
         *
         * @private
         * @property {AudioBuffer} audioBuffer
         */
        this.audioBuffer = manager.game.cache.audio.get(key);
        if (!this.audioBuffer) {
            console.error('No audio loaded in cache with key: \'' + key + '\'!');
            return;
        }
        /**
         * [description]
         *
         * @private
         * @property {AudioBufferSourceNode} source
         */
        this.source = null;
        /**
         * [description]
         *
         * @private
         * @property {GainNode} muteNode
         */
        this.muteNode = manager.context.createGain();
        /**
         * [description]
         *
         * @private
         * @property {GainNode} volumeNode
         */
        this.volumeNode = manager.context.createGain();
        /**
         * The time the previous playback started at based on
         * BaseAudioContext.currentTime value.
         *
         * @private
         * @property {number} startTime
         */
        this.startTime = 0;
        /**
         * An array where we keep track of all rate updates during playback.
         *
         * @private
         * @property {{ time: number, rate: number }[]} rateUpdates
         */
        this.rateUpdates = [];
        /**
         * Used for keeping track when sound source playback has ended
         * so it's state can be updated accordingly.
         *
         * @private
         * @property {boolean} hasEnded
         */
        this.hasEnded = false;
        this.muteNode.connect(this.volumeNode);
        this.volumeNode.connect(manager.destination);
        this.duration = this.audioBuffer.duration;
        this.totalDuration = this.audioBuffer.duration;
        BaseSound.call(this, manager, key, config);
    },
    play: function (markerName, config) {
        if (!BaseSound.prototype.play.call(this, markerName, config)) {
            return null;
        }
        //  \/\/\/ isPlaying = true, isPaused = false \/\/\/
        this.stopAndRemoveBufferSource();
        this.createAndStartBufferSource();
        return this;
    },
    pause: function () {
        if (!BaseSound.prototype.pause.call(this)) {
            return false;
        }
        //  \/\/\/ isPlaying = false, isPaused = true \/\/\/
        this.currentConfig.seek = this.getCurrentTime(); // Equivalent to setting paused time
        this.stopAndRemoveBufferSource();
        return true;
    },
    resume: function () {
        if (!BaseSound.prototype.resume.call(this)) {
            return false;
        }
        //  \/\/\/ isPlaying = true, isPaused = false \/\/\/
        this.createAndStartBufferSource();
        return true;
    },
    stop: function () {
        if (!BaseSound.prototype.stop.call(this)) {
            return false;
        }
        //  \/\/\/ isPlaying = false, isPaused = false \/\/\/
        this.stopAndRemoveBufferSource();
        return true;
    },
    /**
     * Used internally to do what the name says.
     *
     * @private
     */
    // TODO add delay param
    createAndStartBufferSource: function () {
        var _this = this;
        var seek = this.currentConfig.seek;
        var offset = (this.currentMarker ? this.currentMarker.start : 0) + seek;
        var duration = this.duration - seek;
        this.startTime = this.manager.context.currentTime - seek;
        this.source = this.manager.context.createBufferSource();
        this.source.buffer = this.audioBuffer;
        this.source.connect(this.muteNode);
        this.source.onended = function (ev) {
            if (ev.target === _this.source) {
                // sound ended
                _this.hasEnded = true;
            }
            // else was stopped
        };
        this.applyConfig();
        this.source.start(0, Math.max(0, offset), Math.max(0, duration));
        this.resetConfig();
    },
    /**
     * Used internally to do what the name says.
     *
     * @private
     */
    stopAndRemoveBufferSource: function () {
        if (this.source) {
            this.source.stop();
            this.source = null;
        }
        this.startTime = 0;
    },
    /**
     * @protected
     */
    applyConfig: function () {
        this.rateUpdates.length = 0;
        this.rateUpdates.push({
            time: 0,
            rate: 1
        });
        BaseSound.prototype.applyConfig.call(this);
    },
    /**
     * Update method called on every game step.
     *
     * @param {number} time - The current timestamp as generated by the Request Animation Frame or SetTimeout.
     * @param {number} delta - The delta time elapsed since the last frame.
     */
    update: function (time, delta) {
        if (this.hasEnded) {
            this.hasEnded = false;
            this.stop();
        }
    },
    destroy: function () {
        BaseSound.prototype.destroy.call(this);
        this.audioBuffer = null;
        this.stopAndRemoveBufferSource();
        this.muteNode.disconnect();
        this.muteNode = null;
        this.volumeNode.disconnect();
        this.volumeNode = null;
        this.rateUpdates = null;
    },
    /**
     * @private
     */
    setRate: function () {
        BaseSound.prototype.setRate.call(this);
        if (this.source) {
            this.source.playbackRate.setValueAtTime(this.totalRate, 0);
        }
        if (this.isPlaying) {
            this.rateUpdates.push({
                time: this.manager.context.currentTime - this.startTime,
                rate: this.totalRate
            });
        }
    },
    /**
     * @private
     */
    getCurrentTime: function () {
        var currentTime = 0;
        for (var i = 0; i < this.rateUpdates.length; i++) {
            var nextTime = void 0;
            if (i < this.rateUpdates.length - 1) {
                nextTime = this.rateUpdates[i + 1].time;
            }
            else {
                nextTime = this.manager.context.currentTime - this.startTime;
            }
            currentTime += (nextTime - this.rateUpdates[i].time) * this.rateUpdates[i].rate;
        }
        return currentTime;
    }
});
/**
 * Mute setting.
 * @property {boolean} mute
 */
Object.defineProperty(WebAudioSound.prototype, 'mute', {
    get: function () {
        return this.muteNode.gain.value === 0;
    },
    set: function (value) {
        this.currentConfig.mute = value;
        this.muteNode.gain.setValueAtTime(value ? 0 : 1, 0);
    }
});
/**
 * Volume setting.
 * @property {number} volume
 */
Object.defineProperty(WebAudioSound.prototype, 'volume', {
    get: function () {
        return this.volumeNode.gain.value;
    },
    set: function (value) {
        this.currentConfig.volume = value;
        this.volumeNode.gain.setValueAtTime(value, 0);
    }
});
/**
 * Playback rate.
 * @property {number} rate
 */
Object.defineProperty(WebAudioSound.prototype, 'rate', {
    get: function () {
        return this.currentConfig.rate;
    },
    set: function (value) {
        this.currentConfig.rate = value;
        this.setRate();
    }
});
/**
 * Detuning of sound.
 * @property {number} detune
 */
Object.defineProperty(WebAudioSound.prototype, 'detune', {
    get: function () {
        return this.currentConfig.detune;
    },
    set: function (value) {
        this.currentConfig.detune = value;
        this.setRate();
    }
});
/**
 * Current position of playing sound.
 * @property {number} seek
 */
Object.defineProperty(WebAudioSound.prototype, 'seek', {
    get: function () {
        if (this.isPlaying) {
            return this.getCurrentTime();
        }
        else if (this.isPaused) {
            return this.currentConfig.seek;
        }
        else {
            return 0;
        }
    },
    set: function (value) {
        if (this.isPlaying || this.isPaused) {
            value = Math.min(Math.max(0, value), this.duration);
            this.currentConfig.seek = value;
        }
        if (this.isPlaying) {
            this.stopAndRemoveBufferSource();
            this.createAndStartBufferSource();
        }
    }
});
/**
 * Property indicating whether or not
 * the sound or current sound marker will loop.
 *
 * @property {boolean} loop
 */
Object.defineProperty(WebAudioSound.prototype, 'loop', {
    get: function () {
        return this.currentConfig.loop;
    },
    set: function (value) {
        if (value === this.currentConfig.loop) {
            return;
        }
        this.currentConfig.loop = value;
        if (this.isPlaying) {
            if (value) {
                // TODO Add loop source
            }
            else {
                // TODO Remove loop source
            }
        }
    }
});
module.exports = WebAudioSound;
<commit_msg>Checking if looping is enabled when sound ends and if it is creating and playing source sound again<commit_after>var Class = require('../../utils/Class');
var BaseSound = require('../BaseSound');
//  Phaser.Sound.WebAudioSound
// TODO support webkitAudioContext implementation differences
// https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API/Porting_webkitAudioContext_code_to_standards_based_AudioContext
var WebAudioSound = new Class({
    Extends: BaseSound,
    initialize: function WebAudioSound(manager, key, config) {
        if (config === void 0) { config = {}; }
        /**
         * [description]
         *
         * @private
         * @property {AudioBuffer} audioBuffer
         */
        this.audioBuffer = manager.game.cache.audio.get(key);
        if (!this.audioBuffer) {
            console.error('No audio loaded in cache with key: \'' + key + '\'!');
            return;
        }
        /**
         * [description]
         *
         * @private
         * @property {AudioBufferSourceNode} source
         */
        this.source = null;
        /**
         * [description]
         *
         * @private
         * @property {GainNode} muteNode
         */
        this.muteNode = manager.context.createGain();
        /**
         * [description]
         *
         * @private
         * @property {GainNode} volumeNode
         */
        this.volumeNode = manager.context.createGain();
        /**
         * The time the previous playback started at based on
         * BaseAudioContext.currentTime value.
         *
         * @private
         * @property {number} startTime
         */
        this.startTime = 0;
        /**
         * An array where we keep track of all rate updates during playback.
         *
         * @private
         * @property {{ time: number, rate: number }[]} rateUpdates
         */
        this.rateUpdates = [];
        /**
         * Used for keeping track when sound source playback has ended
         * so it's state can be updated accordingly.
         *
         * @private
         * @property {boolean} hasEnded
         */
        this.hasEnded = false;
        this.muteNode.connect(this.volumeNode);
        this.volumeNode.connect(manager.destination);
        this.duration = this.audioBuffer.duration;
        this.totalDuration = this.audioBuffer.duration;
        BaseSound.call(this, manager, key, config);
    },
    play: function (markerName, config) {
        if (!BaseSound.prototype.play.call(this, markerName, config)) {
            return null;
        }
        //  \/\/\/ isPlaying = true, isPaused = false \/\/\/
        this.stopAndRemoveBufferSource();
        this.createAndStartBufferSource();
        return this;
    },
    pause: function () {
        if (!BaseSound.prototype.pause.call(this)) {
            return false;
        }
        //  \/\/\/ isPlaying = false, isPaused = true \/\/\/
        this.currentConfig.seek = this.getCurrentTime(); // Equivalent to setting paused time
        this.stopAndRemoveBufferSource();
        return true;
    },
    resume: function () {
        if (!BaseSound.prototype.resume.call(this)) {
            return false;
        }
        //  \/\/\/ isPlaying = true, isPaused = false \/\/\/
        this.createAndStartBufferSource();
        return true;
    },
    stop: function () {
        if (!BaseSound.prototype.stop.call(this)) {
            return false;
        }
        //  \/\/\/ isPlaying = false, isPaused = false \/\/\/
        this.stopAndRemoveBufferSource();
        return true;
    },
    /**
     * Used internally to do what the name says.
     *
     * @private
     */
    // TODO add delay param
    createAndStartBufferSource: function () {
        var _this = this;
        var seek = this.currentConfig.seek;
        var offset = (this.currentMarker ? this.currentMarker.start : 0) + seek;
        var duration = this.duration - seek;
        this.startTime = this.manager.context.currentTime - seek;
        this.source = this.manager.context.createBufferSource();
        this.source.buffer = this.audioBuffer;
        this.source.connect(this.muteNode);
        this.source.onended = function (ev) {
            if (ev.target === _this.source) {
                // sound ended
                if (_this.currentConfig.loop) {
                    _this.createAndStartBufferSource();
                }
                else {
                    _this.hasEnded = true;
                }
            }
            // else was stopped
        };
        this.applyConfig();
        this.source.start(0, Math.max(0, offset), Math.max(0, duration));
        this.resetConfig();
    },
    /**
     * Used internally to do what the name says.
     *
     * @private
     */
    stopAndRemoveBufferSource: function () {
        if (this.source) {
            this.source.stop();
            this.source = null;
        }
        this.startTime = 0;
    },
    /**
     * @protected
     */
    applyConfig: function () {
        this.rateUpdates.length = 0;
        this.rateUpdates.push({
            time: 0,
            rate: 1
        });
        BaseSound.prototype.applyConfig.call(this);
    },
    /**
     * Update method called on every game step.
     *
     * @param {number} time - The current timestamp as generated by the Request Animation Frame or SetTimeout.
     * @param {number} delta - The delta time elapsed since the last frame.
     */
    update: function (time, delta) {
        if (this.hasEnded) {
            this.hasEnded = false;
            this.stop();
        }
    },
    destroy: function () {
        BaseSound.prototype.destroy.call(this);
        this.audioBuffer = null;
        this.stopAndRemoveBufferSource();
        this.muteNode.disconnect();
        this.muteNode = null;
        this.volumeNode.disconnect();
        this.volumeNode = null;
        this.rateUpdates = null;
    },
    /**
     * @private
     */
    setRate: function () {
        BaseSound.prototype.setRate.call(this);
        if (this.source) {
            this.source.playbackRate.setValueAtTime(this.totalRate, 0);
        }
        if (this.isPlaying) {
            this.rateUpdates.push({
                time: this.manager.context.currentTime - this.startTime,
                rate: this.totalRate
            });
        }
    },
    /**
     * @private
     */
    getCurrentTime: function () {
        var currentTime = 0;
        for (var i = 0; i < this.rateUpdates.length; i++) {
            var nextTime = void 0;
            if (i < this.rateUpdates.length - 1) {
                nextTime = this.rateUpdates[i + 1].time;
            }
            else {
                nextTime = this.manager.context.currentTime - this.startTime;
            }
            currentTime += (nextTime - this.rateUpdates[i].time) * this.rateUpdates[i].rate;
        }
        return currentTime;
    }
});
/**
 * Mute setting.
 * @property {boolean} mute
 */
Object.defineProperty(WebAudioSound.prototype, 'mute', {
    get: function () {
        return this.muteNode.gain.value === 0;
    },
    set: function (value) {
        this.currentConfig.mute = value;
        this.muteNode.gain.setValueAtTime(value ? 0 : 1, 0);
    }
});
/**
 * Volume setting.
 * @property {number} volume
 */
Object.defineProperty(WebAudioSound.prototype, 'volume', {
    get: function () {
        return this.volumeNode.gain.value;
    },
    set: function (value) {
        this.currentConfig.volume = value;
        this.volumeNode.gain.setValueAtTime(value, 0);
    }
});
/**
 * Playback rate.
 * @property {number} rate
 */
Object.defineProperty(WebAudioSound.prototype, 'rate', {
    get: function () {
        return this.currentConfig.rate;
    },
    set: function (value) {
        this.currentConfig.rate = value;
        this.setRate();
    }
});
/**
 * Detuning of sound.
 * @property {number} detune
 */
Object.defineProperty(WebAudioSound.prototype, 'detune', {
    get: function () {
        return this.currentConfig.detune;
    },
    set: function (value) {
        this.currentConfig.detune = value;
        this.setRate();
    }
});
/**
 * Current position of playing sound.
 * @property {number} seek
 */
Object.defineProperty(WebAudioSound.prototype, 'seek', {
    get: function () {
        if (this.isPlaying) {
            return this.getCurrentTime();
        }
        else if (this.isPaused) {
            return this.currentConfig.seek;
        }
        else {
            return 0;
        }
    },
    set: function (value) {
        if (this.isPlaying || this.isPaused) {
            value = Math.min(Math.max(0, value), this.duration);
            this.currentConfig.seek = value;
        }
        if (this.isPlaying) {
            this.stopAndRemoveBufferSource();
            this.createAndStartBufferSource();
        }
    }
});
/**
 * Property indicating whether or not
 * the sound or current sound marker will loop.
 *
 * @property {boolean} loop
 */
Object.defineProperty(WebAudioSound.prototype, 'loop', {
    get: function () {
        return this.currentConfig.loop;
    },
    set: function (value) {
        if (value === this.currentConfig.loop) {
            return;
        }
        this.currentConfig.loop = value;
        if (this.isPlaying) {
            if (value) {
                // TODO Add loop source
            }
            else {
                // TODO Remove loop source
            }
        }
    }
});
module.exports = WebAudioSound;
<|endoftext|>


<commit_before>import 'browser-polyfills';
import 'abortcontroller-polyfill/dist/polyfill-patch-fetch';

// polyfill requestIdleCallback
window.requestIdleCallback = window.requestIdleCallback ||
    function(cb) {
        var start = Date.now();
        return window.requestAnimationFrame(function() {
            cb({
                didTimeout: false,
                timeRemaining: function() {
                    return Math.max(0, 50 - (Date.now() - start));
                }
            });
        });
    };

window.cancelIdleCallback = window.cancelIdleCallback ||
    function(id) {
        window.cancelAnimationFrame(id);
    };

import * as iD from './index';
window.iD = iD;
export default iD
<commit_msg>chore: lint<commit_after>import 'browser-polyfills';
import 'abortcontroller-polyfill/dist/polyfill-patch-fetch';

// polyfill requestIdleCallback
window.requestIdleCallback = window.requestIdleCallback ||
    function(cb) {
        var start = Date.now();
        return window.requestAnimationFrame(function() {
            cb({
                didTimeout: false,
                timeRemaining: function() {
                    return Math.max(0, 50 - (Date.now() - start));
                }
            });
        });
    };

window.cancelIdleCallback = window.cancelIdleCallback ||
    function(id) {
        window.cancelAnimationFrame(id);
    };

import * as iD from './index';
window.iD = iD;
export default iD;
<|endoftext|>


<commit_before>import React from 'react';
import { Button, Col, Form, Row } from 'react-bootstrap';
import { getField, writeActivity } from './activityfns.js';
import * as DB from '../../constants/database.js'
import { countryList } from '../../constants/countries.js';
import * as time from '../Utils/time.js';

/**
 * The form that's used when the user is editing an activity.
 *
 * @param {Object} props This component expects the following props:
 * - `activity` The activity to display.
 * - `submitFunction` The function to run upon submission to trigger card flip.
 */
class EditActivity extends React.Component {
  /** @inheritdoc */
  constructor(props){
    super(props);

    this.state = {startTz: false, endTz: false};

    // Bind state users/modifiers to `this`.
    this.editActivity = this.editActivity.bind(this);
    this.finishEditActivity = this.finishEditActivity.bind(this);
    this.timezoneDropdown = this.timezoneDropdown.bind(this);

    // References.
    this.editTitleRef = React.createRef();
    this.editStartDateRef = React.createRef();
    this.editEndDateRef = React.createRef();
    this.editStartTimeRef = React.createRef();
    this.editEndTimeRef = React.createRef();
    this.editDescriptionRef = React.createRef();
    this.editStartLocRef = React.createRef();
    this.editEndLocRef = React.createRef();
    this.startTz = React.createRef();
    this.endTz = React.createRef();
  }

  /**
   * Edit an activity in the database upon form submission.
   * TODO: Update times as well! This only does the text field forms (#64).
   */
  editActivity() {
    let newVals = {};
    if (this.editTitleRef.current.value !== '') {
      newVals[DB.ACTIVITIES_TITLE] = this.editTitleRef.current.value;
    }
    if (this.editDescriptionRef.current.value !== '') {
      newVals[DB.ACTIVITIES_DESCRIPTION] = this.editDescriptionRef.current.value;
    }
    if (this.editStartLocRef.current.value !== 'No Change'){
      newVals[DB.ACTIVITIES_START_COUNTRY] = this.editStartLocRef.current.value;
    }
    if (this.editEndLocRef.current.value !== 'No Change'){
      newVals[DB.ACTIVITIES_END_COUNTRY] = this.editEndLocRef.current.value;
    }
    if (Object.keys(newVals).length !== 0) {
      writeActivity(this.props.activity.tripId, this.props.activity.id, newVals);
    }
  }

  /** Runs when the `submit` button on the form is pressed.  */
  finishEditActivity(event) {
    event.preventDefault();
    this.editActivity();
    this.props.submitFunction();
  }

  // "Flip switch" on timezone dropdown so the dropdown's contents update to the
  // selected country's timezones.
  startTimeTzUpdate = () => { this.setState({startTz : !this.state.startTz})};
  endTimeTzUpdate = () => { this.setState({endTz : !this.state.endTz})};

  /**
   * Returns a dropdown of all the timezones.
   * The dropdown's values change based on the corrresponding country dropdown to
   * reduce scrolling and ensure that the location corresponds to the time zone.
   *
   * @param st Either 'start' or 'end' depending on whether the
   * timezone is for the start or end timezone.
   *
   * Tests done manually using UI.
   */
  timezoneDropdown(st) {
    let ref = st === 'start' ? this.editStartLocRef : this.editEndLocRef;
    let dbEntry = st === 'start' ? DB.ACTIVITIES_START_COUNTRY : DB.ACTIVITIES_END_COUNTRY;
    let timezones;
    if (ref.current == null) {
      // If activity[key] DNE, then timezones will just return all tzs anyway
      timezones = time.timezonesForCountry(this.props.activity[dbEntry]);
    } else {
      timezones = time.timezonesForCountry(ref.current.value);
    }
    return (
      <div>
      <Form.Control as='select'
        ref={st === 'start' ? this.startTz : this.endTz}
        key={st === 'start' ? this.state.startTz : this.state.endTz}
      >
        {timezones.map((item, index) => {
          return (<option key={index}>{item}</option>);
        })}
      </Form.Control>
      </div>
    )
  }
  /**
   * Create a dropdown of all the countries.
   * This dropdown is linked to the corresponding timezone dropdown,
   * so when the country changes here, the values in the timezone dropdown
   * change as well.
   *
   * @param ref The reference to attach to the dropdown.
   * @param tzref The corresponding time zone reference field.
   */
  countriesDropdown(ref, tzref) {
    return (
      <Form.Control as='select' ref={ref} onChange={tzref}>
        <option key='-1'>No Change</option>
        {countryList.map((item, index) => {
          return (
            <option key={index} eventKey={index}>{item}</option>
          );
        })}
      </Form.Control>
    );
  }

  render() {
    const activity = this.props.activity;
    const TITLEWIDTH = 3;
    const COUNTRYWIDTH = 6;
    const DATEWIDTH = 4;
    const TIMEWIDTH = 2;
    const TZPICKERWIDTH = 3;
    return (
      <Form className='activity-editor' onSubmit={this.finishEditActivity}>
        <Form.Group as={Row} controlId='formActivityTitle'>
          <Col sm={TITLEWIDTH}><Form.Label>Title:</Form.Label></Col>
          <Col><Form.Control type='text' placeholder={activity[DB.ACTIVITIES_TITLE]} ref={this.editTitleRef}/></Col>
        </Form.Group>
        <Form.Group as={Row} controlId='formActivityStartLocation'>
          <Col sm={TITLEWIDTH}><Form.Label>Start Location:</Form.Label></Col>
          <Col sm={COUNTRYWIDTH}>{this.countriesDropdown(this.editStartLocRef, this.startTimeTzUpdate)}</Col>
        </Form.Group>
        <Form.Group as={Row} controlId='formActivityStartLocation'>
          <Col sm={TITLEWIDTH}><Form.Label>End Location:</Form.Label></Col>
          <Col sm={COUNTRYWIDTH}>{this.countriesDropdown(this.editEndLocRef, this.endTimeTzUpdate)}</Col>
        </Form.Group>
        <Form.Group as={Row} controlId='formActivityStartTime'>
          <Col sm={TITLEWIDTH}><Form.Label>From:</Form.Label></Col>
          <Col sm={DATEWIDTH}><Form.Control type='date' label='date' ref={this.editStartDateRef}/></Col>
          <Col sm={TIMEWIDTH}><Form.Control type='time' label='time' ref={this.editStartTimeRef}/></Col>
          <Col sm={TZPICKERWIDTH}>{this.timezoneDropdown('start')}</Col>
        </Form.Group>
        <Form.Group as={Row} controlId='formActivityEndTime'>
          <Col sm={TITLEWIDTH}><Form.Label>To:</Form.Label></Col>
          <Col sm={DATEWIDTH}><Form.Control type='date' label='date' ref={this.editEndDateRef}/></Col>
          <Col sm={TIMEWIDTH}><Form.Control type='time' label='time' ref={this.editEndTimeRef}/></Col>
          <Col sm={TZPICKERWIDTH}>{this.timezoneDropdown('end')}</Col>
        </Form.Group>
        <Form.Group as={Row} controlId='formActivityTitle'>
          <Col sm={TITLEWIDTH}><Form.Label>Description:</Form.Label></Col>
          <Col><Form.Control type='text'
            placeholder={getField(activity, DB.ACTIVITIES_DESCRIPTION, 'Add some details!')}
            ref={this.editDescriptionRef} />
          </Col>
        </Form.Group>
        <Button type='submit' className='float-right'>Done!</Button>
      </Form>
    );
  }
}

export default EditActivity;
<commit_msg>Update comments.<commit_after>import React from 'react';
import { Button, Col, Form, Row } from 'react-bootstrap';
import { getField, writeActivity } from './activityfns.js';
import * as DB from '../../constants/database.js'
import { countryList } from '../../constants/countries.js';
import * as time from '../Utils/time.js';

/**
 * The form that's used when the user is editing an activity.
 *
 * @param {Object} props This component expects the following props:
 * - `activity` The activity to display.
 * - `submitFunction` The function to run upon submission to trigger card flip.
 */
class EditActivity extends React.Component {
  /** @inheritdoc */
  constructor(props){
    super(props);

    this.state = {startTz: false, endTz: false};

    // Bind state users/modifiers to `this`.
    this.editActivity = this.editActivity.bind(this);
    this.finishEditActivity = this.finishEditActivity.bind(this);
    this.timezoneDropdown = this.timezoneDropdown.bind(this);

    // References.
    this.editTitleRef = React.createRef();
    this.editStartDateRef = React.createRef();
    this.editEndDateRef = React.createRef();
    this.editStartTimeRef = React.createRef();
    this.editEndTimeRef = React.createRef();
    this.editDescriptionRef = React.createRef();
    this.editStartLocRef = React.createRef();
    this.editEndLocRef = React.createRef();
    this.startTz = React.createRef();
    this.endTz = React.createRef();
  }

  /**
   * Edit an activity in the database upon form submission.
   * TODO: Update times as well! This only does the text field forms (#64).
   */
  editActivity() {
    let newVals = {};
    if (this.editTitleRef.current.value !== '') {
      newVals[DB.ACTIVITIES_TITLE] = this.editTitleRef.current.value;
    }
    if (this.editDescriptionRef.current.value !== '') {
      newVals[DB.ACTIVITIES_DESCRIPTION] = this.editDescriptionRef.current.value;
    }
    if (this.editStartLocRef.current.value !== 'No Change'){
      newVals[DB.ACTIVITIES_START_COUNTRY] = this.editStartLocRef.current.value;
    }
    if (this.editEndLocRef.current.value !== 'No Change'){
      newVals[DB.ACTIVITIES_END_COUNTRY] = this.editEndLocRef.current.value;
    }
    if (Object.keys(newVals).length !== 0) {
      writeActivity(this.props.activity.tripId, this.props.activity.id, newVals);
    }
  }

  /** Runs when the `submit` button on the form is pressed.  */
  finishEditActivity(event) {
    event.preventDefault();
    this.editActivity();
    this.props.submitFunction();
  }

  // "Flip switch" on timezone dropdown so the dropdown's contents update to the
  // selected country's timezones.
  startTimeTzUpdate = () => { this.setState({startTz : !this.state.startTz})};
  endTimeTzUpdate = () => { this.setState({endTz : !this.state.endTz})};

  /**
   * Returns a dropdown of all the timezones.
   * The dropdown's values change based on the corrresponding country dropdown to
   * reduce scrolling and ensure that the location corresponds to the time zone.
   *
   * Tests done manually using UI.
   *
   * @param st Either 'start' or 'end' depending on whether the
   * timezone is for the start or end timezone.
   * @returns HTML dropdown item.
   */
  timezoneDropdown(st) {
    let ref = st === 'start' ? this.editStartLocRef : this.editEndLocRef;
    let dbEntry = st === 'start' ? DB.ACTIVITIES_START_COUNTRY : DB.ACTIVITIES_END_COUNTRY;
    let timezones;
    if (ref.current == null) {
      // If activity[key] DNE, then timezones will just return all tzs anyway
      timezones = time.timezonesForCountry(this.props.activity[dbEntry]);
    } else {
      timezones = time.timezonesForCountry(ref.current.value);
    }
    return (
      <Form.Control as='select'
        ref={st === 'start' ? this.startTz : this.endTz}
        key={st === 'start' ? this.state.startTz : this.state.endTz}
      >
        {timezones.map((item, index) => {
          return (<option key={index}>{item}</option>);
        })}
      </Form.Control>
    )
  }
  /**
   * Create a dropdown of all the countries.
   * This dropdown is linked to the corresponding timezone dropdown,
   * so when the country changes here, the values in the timezone dropdown
   * change as well.
   *
   * @param ref The reference to attach to the dropdown.
   * @param tzref The corresponding time zone reference field.
   */
  countriesDropdown(ref, tzref) {
    return (
      <Form.Control as='select' ref={ref} onChange={tzref}>
        <option key='-1'>No Change</option>
        {countryList.map((item, index) => {
          return (
            <option key={index} eventKey={index}>{item}</option>
          );
        })}
      </Form.Control>
    );
  }

  render() {
    const activity = this.props.activity;
    const TITLEWIDTH = 3;
    const COUNTRYWIDTH = 6;
    const DATEWIDTH = 4;
    const TIMEWIDTH = 2;
    const TZPICKERWIDTH = 3;
    return (
      <Form className='activity-editor' onSubmit={this.finishEditActivity}>
        <Form.Group as={Row} controlId='formActivityTitle'>
          <Col sm={TITLEWIDTH}><Form.Label>Title:</Form.Label></Col>
          <Col><Form.Control type='text' placeholder={activity[DB.ACTIVITIES_TITLE]} ref={this.editTitleRef}/></Col>
        </Form.Group>
        <Form.Group as={Row} controlId='formActivityStartLocation'>
          <Col sm={TITLEWIDTH}><Form.Label>Start Location:</Form.Label></Col>
          <Col sm={COUNTRYWIDTH}>{this.countriesDropdown(this.editStartLocRef, this.startTimeTzUpdate)}</Col>
        </Form.Group>
        <Form.Group as={Row} controlId='formActivityStartLocation'>
          <Col sm={TITLEWIDTH}><Form.Label>End Location:</Form.Label></Col>
          <Col sm={COUNTRYWIDTH}>{this.countriesDropdown(this.editEndLocRef, this.endTimeTzUpdate)}</Col>
        </Form.Group>
        <Form.Group as={Row} controlId='formActivityStartTime'>
          <Col sm={TITLEWIDTH}><Form.Label>From:</Form.Label></Col>
          <Col sm={DATEWIDTH}><Form.Control type='date' label='date' ref={this.editStartDateRef}/></Col>
          <Col sm={TIMEWIDTH}><Form.Control type='time' label='time' ref={this.editStartTimeRef}/></Col>
          <Col sm={TZPICKERWIDTH}>{this.timezoneDropdown('start')}</Col>
        </Form.Group>
        <Form.Group as={Row} controlId='formActivityEndTime'>
          <Col sm={TITLEWIDTH}><Form.Label>To:</Form.Label></Col>
          <Col sm={DATEWIDTH}><Form.Control type='date' label='date' ref={this.editEndDateRef}/></Col>
          <Col sm={TIMEWIDTH}><Form.Control type='time' label='time' ref={this.editEndTimeRef}/></Col>
          <Col sm={TZPICKERWIDTH}>{this.timezoneDropdown('end')}</Col>
        </Form.Group>
        <Form.Group as={Row} controlId='formActivityTitle'>
          <Col sm={TITLEWIDTH}><Form.Label>Description:</Form.Label></Col>
          <Col><Form.Control type='text'
            placeholder={getField(activity, DB.ACTIVITIES_DESCRIPTION, 'Add some details!')}
            ref={this.editDescriptionRef} />
          </Col>
        </Form.Group>
        <Button type='submit' className='float-right'>Done!</Button>
      </Form>
    );
  }
}

export default EditActivity;
<|endoftext|>


<commit_before>/* Services */

var ernest = angular.module('ernest');

// To do: this should pull from the server.
ernest.value('version', '0.1a1');

ernest.factory('Api', ['$resource', 'localStorageService',
    function($resource, localStorageService) {

        function bugUrl() {
            return 'https://bugzilla.mozilla.org/show_bug.cgi?id=' + this.id;
        }

        function augmentProject(proj) {
            proj.url = '/project/' + proj.slug;
            return proj;
        }

        function augmentSprint(project, sprint, bugs) {
            var i,
                buglist_all = [],
                buglist_open = [],
                today = new Date();

            sprint.project = project;
            sprint.url = '/project/' + project.slug + '/' + sprint.slug;

            if (today >= new Date(sprint.start_date) && today <= new Date(sprint.end_date)) {
                sprint.current = true;
            } else {
                sprint.current = false;
            }

            // bugzilla buglist urls
            if (bugs) {
                for (i = 0; i < bugs.length; i++) {
                    buglist_all.push(bugs[i].id);
                    if (bugs[i].status.toLowerCase() !== 'resolved'
                        && bugs[i].status.toLowerCase() !== 'verified') {

                        buglist_open.push(bugs[i].id);
                    }
                }
                sprint.buglist_all_url = 'https://bugzilla.mozilla.org/buglist.cgi?bug_id=' +
                    buglist_all.join(',');

                sprint.buglist_open_url = 'https://bugzilla.mozilla.org/buglist.cgi?bug_id=' +
                    buglist_open.join(',');
            }

            return sprint;
        }

        function augmentBug(bug, sprint) {
            var baseUrl = 'https://bugzilla.mozilla.org/show_bug.cgi?id=';
            bug.url = baseUrl + bug.id;
            if (bug.open_blockers) {
                bug.open_blockers = bug.open_blockers.map(function(bugId) {
                    return {
                        id: bugId,
                        url: baseUrl + bugId,
                    };
                });
            }
            bug.details_url = '/bugzilla/bug/' + bug.id;

            if (bug.points !== null && sprint !== undefined && sprint.end_date !== null) {
                // If the bug has points allocated and we know the
                // sprint end_date, then we can calculate whether the
                // bug is "in jeopardy".

                // FIXME - This is hard-coded for now. It should be part of
                // the sprint data.
                var pointsToHours = {
                    0: {start: 1, end: 1},
                    1: {start: 12, end: 12},
                    2: {start: 24, end: 48},
                    3: {start: 72, end: 120}
                };

                var endDate = new Date(sprint.end_date);
                endDate.setUTCHours(23, 59, 59, 999);

                var getBound = function (hours) {
                    var now = new Date();
                    now.setUTCHours(now.getUTCHours() + hours);
                    return now;
                };

                if (pointsToHours[bug.points] !== undefined) {
                    var startBound = getBound(pointsToHours[bug.points].start);
                    var endBound = getBound(pointsToHours[bug.points].end);

                    if (startBound > endDate) {
                        bug.jeopardy = 'error';
                    }
                    if (endBound > endDate) {
                        bug.jeopardy = 'warning';
                    }
                }
            }

            bug.star = localStorageService.get('star' + bug.id) || '0';

            return bug;
        }

        function augment(data, headersGetter) {
            if (data.projects) {
                data.projects = data.projects.map(augmentProject);
            }
            if (data.project) {
                data.project = augmentProject(data.project);
            }
            if (data.trackers) {
                data.trackers = data.trackers.map(augmentBug);
            }
            if (data.sprints) {
                data.sprints = data.sprints.map(augmentSprint.bind(null, data.project));
            }
            if (data.prev_sprint) {
                data.prev_sprint = augmentSprint(data.project, data.prev_sprint);
            }
            if (data.sprint) {
                data.sprint = augmentSprint(data.project, data.sprint, data.bugs);
            }
            if (data.next_sprint) {
                data.next_sprint = augmentSprint(data.project, data.next_sprint);
            }
            if (data.bugs) {
                data.bugs = data.bugs.map(function (bug) { return augmentBug(bug, data.sprint); });
            }
            return data;
        }

        return $resource('/api/project/:projSlug/:sprintSlug/', {}, {
            query: {
                method: 'GET',
                isArray: false,
                transformResponse: [augment],
                cache: true,
                responseType: 'json'
            },
            get: {
                method: 'GET',
                isArray: false,
                transformResponse: [augment],
                cache: true,
                responseType: 'json'
            }
        });
    }
]);

ernest.factory('BugApi', ['$resource',
    function($resource) {

        function bugUrl() {
            return 'https://bugzilla.mozilla.org/show_bug.cgi?id=' + this.id;
        }

        function augmentBug(bug) {
            var baseUrl = 'https://bugzilla.mozilla.org/show_bug.cgi?id=';
            bug.url = baseUrl + bug.id;
            if (bug.blockers) {
                bug.blockers = bug.blockers.map(function(bug_data) {
                    bug_data.url = baseUrl + bug_data.id;
                    return bug_data;
                });
            }
            bug.details_url = '/bugzilla/bug/' + bug.id;

            return bug;
        }

        function augment(data, headersGetter) {
            if (data.bugs) {
                data.bugs = data.bugs.map(augmentBug);
            }
            if (data.bug) {
                data.bug = augmentBug(data.bug);
            }
            return data;
        }

        return $resource('/api/bugzilla/bug/:bugId', {}, {
            get: {
                method: 'GET',
                isArray: false,
                transformResponse: [augment],
                cache: true,
                responseType: 'json'
            }
        });
    }
]);
<commit_msg>Fix the "some yellow should be red" issue<commit_after>/* Services */

var ernest = angular.module('ernest');

// To do: this should pull from the server.
ernest.value('version', '0.1a1');

ernest.factory('Api', ['$resource', 'localStorageService',
    function($resource, localStorageService) {

        function bugUrl() {
            return 'https://bugzilla.mozilla.org/show_bug.cgi?id=' + this.id;
        }

        function augmentProject(proj) {
            proj.url = '/project/' + proj.slug;
            return proj;
        }

        function augmentSprint(project, sprint, bugs) {
            var i,
                buglist_all = [],
                buglist_open = [],
                today = new Date();

            sprint.project = project;
            sprint.url = '/project/' + project.slug + '/' + sprint.slug;

            if (today >= new Date(sprint.start_date) && today <= new Date(sprint.end_date)) {
                sprint.current = true;
            } else {
                sprint.current = false;
            }

            // bugzilla buglist urls
            if (bugs) {
                for (i = 0; i < bugs.length; i++) {
                    buglist_all.push(bugs[i].id);
                    if (bugs[i].status.toLowerCase() !== 'resolved'
                        && bugs[i].status.toLowerCase() !== 'verified') {

                        buglist_open.push(bugs[i].id);
                    }
                }
                sprint.buglist_all_url = 'https://bugzilla.mozilla.org/buglist.cgi?bug_id=' +
                    buglist_all.join(',');

                sprint.buglist_open_url = 'https://bugzilla.mozilla.org/buglist.cgi?bug_id=' +
                    buglist_open.join(',');
            }

            return sprint;
        }

        function augmentBug(bug, sprint) {
            var baseUrl = 'https://bugzilla.mozilla.org/show_bug.cgi?id=';
            bug.url = baseUrl + bug.id;
            if (bug.open_blockers) {
                bug.open_blockers = bug.open_blockers.map(function(bugId) {
                    return {
                        id: bugId,
                        url: baseUrl + bugId,
                    };
                });
            }
            bug.details_url = '/bugzilla/bug/' + bug.id;

            if (bug.points !== null && sprint !== undefined && sprint.end_date !== null) {
                // If the bug has points allocated and we know the
                // sprint end_date, then we can calculate whether the
                // bug is "in jeopardy".

                // FIXME - This is hard-coded for now. It should be part of
                // the sprint data.
                var pointsToHours = {
                    0: {start: 1, end: 1},
                    1: {start: 12, end: 12},
                    2: {start: 24, end: 48},
                    3: {start: 72, end: 120}
                };

                var endDate = new Date(sprint.end_date);
                endDate.setUTCHours(23, 59, 59, 999);

                var getBound = function (hours) {
                    var now = new Date();
                    now.setUTCHours(now.getUTCHours() + hours);
                    return now;
                };

                if (pointsToHours[bug.points] !== undefined) {
                    var startBound = getBound(pointsToHours[bug.points].start);
                    var endBound = getBound(pointsToHours[bug.points].end);

                    if (endBound > endDate) {
                        bug.jeopardy = 'warning';
                    }
                    if (startBound > endDate) {
                        bug.jeopardy = 'error';
                    }
                }
            }

            bug.star = localStorageService.get('star' + bug.id) || '0';

            return bug;
        }

        function augment(data, headersGetter) {
            if (data.projects) {
                data.projects = data.projects.map(augmentProject);
            }
            if (data.project) {
                data.project = augmentProject(data.project);
            }
            if (data.trackers) {
                data.trackers = data.trackers.map(augmentBug);
            }
            if (data.sprints) {
                data.sprints = data.sprints.map(augmentSprint.bind(null, data.project));
            }
            if (data.prev_sprint) {
                data.prev_sprint = augmentSprint(data.project, data.prev_sprint);
            }
            if (data.sprint) {
                data.sprint = augmentSprint(data.project, data.sprint, data.bugs);
            }
            if (data.next_sprint) {
                data.next_sprint = augmentSprint(data.project, data.next_sprint);
            }
            if (data.bugs) {
                data.bugs = data.bugs.map(function (bug) { return augmentBug(bug, data.sprint); });
            }
            return data;
        }

        return $resource('/api/project/:projSlug/:sprintSlug/', {}, {
            query: {
                method: 'GET',
                isArray: false,
                transformResponse: [augment],
                cache: true,
                responseType: 'json'
            },
            get: {
                method: 'GET',
                isArray: false,
                transformResponse: [augment],
                cache: true,
                responseType: 'json'
            }
        });
    }
]);

ernest.factory('BugApi', ['$resource',
    function($resource) {

        function bugUrl() {
            return 'https://bugzilla.mozilla.org/show_bug.cgi?id=' + this.id;
        }

        function augmentBug(bug) {
            var baseUrl = 'https://bugzilla.mozilla.org/show_bug.cgi?id=';
            bug.url = baseUrl + bug.id;
            if (bug.blockers) {
                bug.blockers = bug.blockers.map(function(bug_data) {
                    bug_data.url = baseUrl + bug_data.id;
                    return bug_data;
                });
            }
            bug.details_url = '/bugzilla/bug/' + bug.id;

            return bug;
        }

        function augment(data, headersGetter) {
            if (data.bugs) {
                data.bugs = data.bugs.map(augmentBug);
            }
            if (data.bug) {
                data.bug = augmentBug(data.bug);
            }
            return data;
        }

        return $resource('/api/bugzilla/bug/:bugId', {}, {
            get: {
                method: 'GET',
                isArray: false,
                transformResponse: [augment],
                cache: true,
                responseType: 'json'
            }
        });
    }
]);
<|endoftext|>


<commit_before>// const { Application } = require('spectron');
const chai = require('chai');
const chaiAsPromised = require('chai-as-promised');
const path = require('path');
// const electron = require('electron');
const { _electron: electron } = require('playwright');

const { logFilter } = require('./helper');

const appPath = path.join(__dirname, '../..');

const EXIST_TIMEOUT = 10000000;
const COMPUTATION_TIMEOUT = 150000;
const COMPUTATION_DOWNLOAD_TIMEOUT = 40000;
const USER_ID = 'test1';
const PASS = 'password';
const CONS_NAME = 'e2e-consortium-single';
const CONS_DESC = 'e2e-description-single';
const PIPE_NAME = 'e2e-pipeline-single';
const PIPE_DESC = 'e2e-pipeline-description-single';
const COMPUTATION_NAME = 'Ridge Regression (Singleshot) - FreeSurfer Volumes';

chai.should();
chai.use(chaiAsPromised);

let appWindow;
let app;

describe('e2e run computation with 1 member', () => {
  before(async () => {
    app = await electron.launch({
      // path: electron,
      args: ['--enable-logging', appPath],
      env: Object.assign({}, process.env, { NODE_ENV: 'test' }),
      logger: {
        isEnabled: (name, severity) => true,
        log: (name, severity, message, args) => console.log(`${name}: ${message}`)
      },
      // chromeDriverArgs: [
      //   '--no-sandbox',
      //   '--whitelisted-ips=',
      //   '--disable-dev-shm-usage',
      // ],
    });
    appWindow = await app.firstWindow();
    appWindow.on('console', msg => console.log(msg.text()));
  });

  // after(async () => {
  //   if (process.env.CI) {
  //     await app.client.getMainProcessLogs().then((logs) => {
  //       logs.filter(logFilter).forEach((log) => {
  //         console.log(log); // eslint-disable-line no-console
  //       });
  //     });
  //   }
  //   if (app && app.isRunning()) {
  //     return app.stop();
  //   }
  // });

  it('displays the correct title', async () => {
    return appWindow.title().should.eventually.equal('COINSTAC');
  });

  it('authenticates demo user', async () => {
    await appWindow.click('#login-username', { timeout: EXIST_TIMEOUT });
    await appWindow.fill('#login-username', USER_ID);
    await appWindow.fill('#login-password', PASS);

    await appWindow.click('button:has-text("Log In")');

    // Assert
    return appWindow.innerText('.user-account-name', { timeout: EXIST_TIMEOUT }).should.eventually.equal(USER_ID);
  });

  it('accesses the Add Consortium page', async () => {
    await appWindow.click('a:has-text("Consortia")');

    await appWindow.click('a[name="create-consortium-button"]', { timeout: EXIST_TIMEOUT });

    // Assert
    return appWindow.waitForSelector('h4:has-text("Consortium Creation")', {
      state: 'visible',
      timeout: EXIST_TIMEOUT,
    }).should.eventually.not.equal(null);
  });

  it('creates a consortium', async () => {
    await appWindow.fill('#name', CONS_NAME);
    await appWindow.fill('#description', CONS_DESC);

    await appWindow.click('button:has-text("Save")');

    // Assert
    await appWindow.waitForSelector('span:has-text("Consortium Saved")', {
      state: 'visible',
      timeout: EXIST_TIMEOUT,
    }).should.eventually.not.equal(null);

    await appWindow.click('a:has-text("Consortia")');

    await appWindow.waitForSelector(`h5:has-text("${CONS_NAME}")`, {
      state: 'visible',
      timeout: EXIST_TIMEOUT,
    }).should.eventually.not.equal(null);
  });

  it('accesses the Add Pipeline page', async () => {
    await appWindow.click('a:has-text("Pipelines")');

    await appWindow.click('a[name="create-pipeline-button"]', { timeout: EXIST_TIMEOUT });

    // Assert
    return appWindow.waitForSelector('h4:has-text("Pipeline Creation")', {
      state: 'visible',
      timeout: EXIST_TIMEOUT,
    }).should.eventually.not.equal(null);
  });

  it('creates a pipeline', async () => {
    await appWindow.fill('#name', PIPE_NAME);
    await appWindow.fill('#description', PIPE_DESC);

    await appWindow.click('#pipelineconsortia', { timeout: EXIST_TIMEOUT });
    await appWindow.click(`#consortium-menu li:has-text("${CONS_NAME}")`, { timeout: EXIST_TIMEOUT });

    await appWindow.click('#computation-dropdown', { timeout: EXIST_TIMEOUT });
    await appWindow.click(`#computation-menu li:has-text("${COMPUTATION_NAME}")`, { timeout: EXIST_TIMEOUT });

    await appWindow.click('.pipeline-step', { timeout: EXIST_TIMEOUT });

    const addCovariate = async (name, type, index) => {
      await appWindow.click('button:has-text("Add Covariates")', { timeout: EXIST_TIMEOUT });

      await appWindow.click(`#covariates-${index}-data-dropdown`, { timeout: EXIST_TIMEOUT });
      await appWindow.click(`#covariates-${index}-data-dropdown-menu li:has-text("${type}")`, { timeout: EXIST_TIMEOUT });

      await appWindow.fill(`#covariates-${index}-input-name`, name);
    };

    await addCovariate('isControl', 'boolean', 0);
    await addCovariate('age', 'number', 1);

    await appWindow.click('button:has-text("Add Data")', { timeout: EXIST_TIMEOUT });

    await appWindow.click('#data-0-data-dropdown', { timeout: EXIST_TIMEOUT });
    await appWindow.click('#data-0-data-dropdown-menu li:has-text("FreeSurfer")', { timeout: EXIST_TIMEOUT });

    await appWindow.click('#data-0-area', { timeout: EXIST_TIMEOUT });
    await appWindow.click('#data-0-area .react-select-dropdown-menu div:has-text("5th-Ventricle")', { timeout: EXIST_TIMEOUT });

    await appWindow.fill('[name="step-lambda"]', '0');

    await appWindow.click('button:has-text("Save Pipeline")', { timeout: EXIST_TIMEOUT });

    // Assert
    await appWindow.waitForSelector('span:has-text("Pipeline Saved")', {
      state: 'visible',
      timeout: EXIST_TIMEOUT,
    }).should.eventually.not.equal(null);
  });

  it('sets the created pipeline to the consortium', async () => {
    await appWindow.click('a:has-text("Consortia")');

    await appWindow.click(`a[name="${CONS_NAME}"]`, { timeout: EXIST_TIMEOUT });

    await appWindow.click('#consortium-tabs span:has-text("Pipelines")', { timeout: EXIST_TIMEOUT });

    await appWindow.click('#owned-pipelines-dropdown', { timeout: EXIST_TIMEOUT });
    await appWindow.click(`#owned-pipelines-dropdown-menu li:has-text("${PIPE_NAME}")`, { timeout: EXIST_TIMEOUT });

    // Assert
    return Promise.all([
      appWindow.waitForSelector(`a:has-text("${PIPE_NAME}")`, {
        state: 'visible',
        timeout: EXIST_TIMEOUT,
      }).should.eventually.not.equal(null),
      appWindow.waitForSelector(`span:has-text("${COMPUTATION_NAME} Download Complete")`, {
        state: 'visible',
        timeout: EXIST_TIMEOUT,
      }).should.eventually.not.equal(null),
    ]);
  });

  it('map data to consortium', async () => {
    await appWindow.click('a:has-text("Maps")', { timeout: EXIST_TIMEOUT });

    await appWindow.click(`a[name="${CONS_NAME}"]`, { timeout: EXIST_TIMEOUT });

    await appWindow.click('button:has-text("Select File(s)")', { timeout: EXIST_TIMEOUT });

    await appWindow.click('button:has-text("Auto Map")', { timeout: EXIST_TIMEOUT });

    await appWindow.click('button:has-text("Save")', { timeout: EXIST_TIMEOUT });

    await appWindow.click('a:has-text("Consortia")', { timeout: EXIST_TIMEOUT });

    // Assert
    return appWindow.waitForSelector('button:has-text("Start Pipeline")', {
      state: 'visible',
      timeout: EXIST_TIMEOUT,
    }).should.eventually.not.equal(null);
  });

  it('runs a computation', async () => {
    await appWindow.click('button:has-text("Start Pipeline")', { timeout: EXIST_TIMEOUT });

    // Assert
    return appWindow.waitForSelector(`span:has-text("Pipeline Starting for ${CONS_NAME}.")`, {
      state: 'visible',
      timeout: EXIST_TIMEOUT,
    }).should.eventually.not.equal(null);
  });

  it('displays computation progress', async () => {
    await appWindow.click('a:has-text("Home")', { timeout: EXIST_TIMEOUT });

    // Assert
    return appWindow.waitForSelector('div.run-item-paper:first-child span:has-text("In Progress")', {
      state: 'visible',
      timeout: EXIST_TIMEOUT,
    }).should.eventually.not.equal(null);
  });

  it('displays results', async () => {
    // Wait for computation to complete (results button only shows up at the end of the run)
    await appWindow.click('div.run-item-paper:first-child a:has-text("View Results")', { timeout: COMPUTATION_TIMEOUT });

    // Assert
    return Promise.all([
      appWindow.waitForSelector('h3:has-text("Regressions")', {
        state: 'visible',
        timeout: EXIST_TIMEOUT,
      }).should.eventually.not.equal(null),
      appWindow.waitForSelector(`h6:has-text("Results: ${CONS_NAME} || ${PIPE_NAME}")`, {
        state: 'visible',
        timeout: EXIST_TIMEOUT,
      }).should.eventually.not.equal(null),
    ]);
  });

  it('deletes consortium', async () => {
    await appWindow.click('a:has-text("Consortia")', { timeout: EXIST_TIMEOUT });

    await appWindow.click(`button[name="${CONS_NAME}-delete"]`, { timeout: EXIST_TIMEOUT });

    await appWindow.click('#list-delete-modal button:has-text("Delete")', { timeout: EXIST_TIMEOUT });

    // Assert

    // Wait for consortium item to be deleted from consortium list
    return appWindow.waitForSelector(`h1:has-text("${CONS_NAME}")`, {
      state: 'hidden',
      timeout: EXIST_TIMEOUT,
    }).should.eventually.equal(null);
  });

  it('logs out', async () => {
    await appWindow.click('a:has-text("Log Out")', { timeout: EXIST_TIMEOUT });

    // Assert
    return appWindow.waitForSelector('button:has-text("Log In")', {
      state: 'visible',
      timeout: EXIST_TIMEOUT,
    }).should.eventually.not.equal(null);
  });
});
<commit_msg>Increase test timeout for computation download<commit_after>// const { Application } = require('spectron');
const chai = require('chai');
const chaiAsPromised = require('chai-as-promised');
const path = require('path');
// const electron = require('electron');
const { _electron: electron } = require('playwright');

const { logFilter } = require('./helper');

const appPath = path.join(__dirname, '../..');

const EXIST_TIMEOUT = 10000000;
const COMPUTATION_TIMEOUT = 150000;
const COMPUTATION_DOWNLOAD_TIMEOUT = 40000;
const USER_ID = 'test1';
const PASS = 'password';
const CONS_NAME = 'e2e-consortium-single';
const CONS_DESC = 'e2e-description-single';
const PIPE_NAME = 'e2e-pipeline-single';
const PIPE_DESC = 'e2e-pipeline-description-single';
const COMPUTATION_NAME = 'Ridge Regression (Singleshot) - FreeSurfer Volumes';

chai.should();
chai.use(chaiAsPromised);

let appWindow;
let app;

describe('e2e run computation with 1 member', () => {
  before(async () => {
    app = await electron.launch({
      // path: electron,
      args: ['--enable-logging', appPath],
      env: Object.assign({}, process.env, { NODE_ENV: 'test' }),
      logger: {
        isEnabled: (name, severity) => true,
        log: (name, severity, message, args) => console.log(`${name}: ${message}`)
      },
      // chromeDriverArgs: [
      //   '--no-sandbox',
      //   '--whitelisted-ips=',
      //   '--disable-dev-shm-usage',
      // ],
    });
    appWindow = await app.firstWindow();
    appWindow.on('console', msg => console.log(msg.text()));
  });

  // after(async () => {
  //   if (process.env.CI) {
  //     await app.client.getMainProcessLogs().then((logs) => {
  //       logs.filter(logFilter).forEach((log) => {
  //         console.log(log); // eslint-disable-line no-console
  //       });
  //     });
  //   }
  //   if (app && app.isRunning()) {
  //     return app.stop();
  //   }
  // });

  it('displays the correct title', async () => {
    return appWindow.title().should.eventually.equal('COINSTAC');
  });

  it('authenticates demo user', async () => {
    await appWindow.click('#login-username', { timeout: EXIST_TIMEOUT });
    await appWindow.fill('#login-username', USER_ID);
    await appWindow.fill('#login-password', PASS);

    await appWindow.click('button:has-text("Log In")');

    // Assert
    return appWindow.innerText('.user-account-name', { timeout: EXIST_TIMEOUT }).should.eventually.equal(USER_ID);
  });

  it('accesses the Add Consortium page', async () => {
    await appWindow.click('a:has-text("Consortia")');

    await appWindow.click('a[name="create-consortium-button"]', { timeout: EXIST_TIMEOUT });

    // Assert
    return appWindow.waitForSelector('h4:has-text("Consortium Creation")', {
      state: 'visible',
      timeout: EXIST_TIMEOUT,
    }).should.eventually.not.equal(null);
  });

  it('creates a consortium', async () => {
    await appWindow.fill('#name', CONS_NAME);
    await appWindow.fill('#description', CONS_DESC);

    await appWindow.click('button:has-text("Save")');

    // Assert
    await appWindow.waitForSelector('span:has-text("Consortium Saved")', {
      state: 'visible',
      timeout: EXIST_TIMEOUT,
    }).should.eventually.not.equal(null);

    await appWindow.click('a:has-text("Consortia")');

    await appWindow.waitForSelector(`h5:has-text("${CONS_NAME}")`, {
      state: 'visible',
      timeout: EXIST_TIMEOUT,
    }).should.eventually.not.equal(null);
  });

  it('accesses the Add Pipeline page', async () => {
    await appWindow.click('a:has-text("Pipelines")');

    await appWindow.click('a[name="create-pipeline-button"]', { timeout: EXIST_TIMEOUT });

    // Assert
    return appWindow.waitForSelector('h4:has-text("Pipeline Creation")', {
      state: 'visible',
      timeout: EXIST_TIMEOUT,
    }).should.eventually.not.equal(null);
  });

  it('creates a pipeline', async () => {
    await appWindow.fill('#name', PIPE_NAME);
    await appWindow.fill('#description', PIPE_DESC);

    await appWindow.click('#pipelineconsortia', { timeout: EXIST_TIMEOUT });
    await appWindow.click(`#consortium-menu li:has-text("${CONS_NAME}")`, { timeout: EXIST_TIMEOUT });

    await appWindow.click('#computation-dropdown', { timeout: EXIST_TIMEOUT });
    await appWindow.click(`#computation-menu li:has-text("${COMPUTATION_NAME}")`, { timeout: EXIST_TIMEOUT });

    await appWindow.click('.pipeline-step', { timeout: EXIST_TIMEOUT });

    const addCovariate = async (name, type, index) => {
      await appWindow.click('button:has-text("Add Covariates")', { timeout: EXIST_TIMEOUT });

      await appWindow.click(`#covariates-${index}-data-dropdown`, { timeout: EXIST_TIMEOUT });
      await appWindow.click(`#covariates-${index}-data-dropdown-menu li:has-text("${type}")`, { timeout: EXIST_TIMEOUT });

      await appWindow.fill(`#covariates-${index}-input-name`, name);
    };

    await addCovariate('isControl', 'boolean', 0);
    await addCovariate('age', 'number', 1);

    await appWindow.click('button:has-text("Add Data")', { timeout: EXIST_TIMEOUT });

    await appWindow.click('#data-0-data-dropdown', { timeout: EXIST_TIMEOUT });
    await appWindow.click('#data-0-data-dropdown-menu li:has-text("FreeSurfer")', { timeout: EXIST_TIMEOUT });

    await appWindow.click('#data-0-area', { timeout: EXIST_TIMEOUT });
    await appWindow.click('#data-0-area .react-select-dropdown-menu div:has-text("5th-Ventricle")', { timeout: EXIST_TIMEOUT });

    await appWindow.fill('[name="step-lambda"]', '0');

    await appWindow.click('button:has-text("Save Pipeline")', { timeout: EXIST_TIMEOUT });

    // Assert
    await appWindow.waitForSelector('span:has-text("Pipeline Saved")', {
      state: 'visible',
      timeout: EXIST_TIMEOUT,
    }).should.eventually.not.equal(null);
  });

  it('sets the created pipeline to the consortium', async () => {
    await appWindow.click('a:has-text("Consortia")');

    await appWindow.click(`a[name="${CONS_NAME}"]`, { timeout: EXIST_TIMEOUT });

    await appWindow.click('#consortium-tabs span:has-text("Pipelines")', { timeout: EXIST_TIMEOUT });

    await appWindow.click('#owned-pipelines-dropdown', { timeout: EXIST_TIMEOUT });
    await appWindow.click(`#owned-pipelines-dropdown-menu li:has-text("${PIPE_NAME}")`, { timeout: EXIST_TIMEOUT });

    // Assert
    return Promise.all([
      appWindow.waitForSelector(`a:has-text("${PIPE_NAME}")`, {
        state: 'visible',
        timeout: EXIST_TIMEOUT,
      }).should.eventually.not.equal(null),
      appWindow.waitForSelector(`span:has-text("${COMPUTATION_NAME} Download Complete")`, {
        state: 'visible',
        timeout: COMPUTATION_DOWNLOAD_TIMEOUT,
      }).should.eventually.not.equal(null),
    ]);
  });

  it('map data to consortium', async () => {
    await appWindow.click('a:has-text("Maps")', { timeout: EXIST_TIMEOUT });

    await appWindow.click(`a[name="${CONS_NAME}"]`, { timeout: EXIST_TIMEOUT });

    await appWindow.click('button:has-text("Select File(s)")', { timeout: EXIST_TIMEOUT });

    await appWindow.click('button:has-text("Auto Map")', { timeout: EXIST_TIMEOUT });

    await appWindow.click('button:has-text("Save")', { timeout: EXIST_TIMEOUT });

    await appWindow.click('a:has-text("Consortia")', { timeout: EXIST_TIMEOUT });

    // Assert
    return appWindow.waitForSelector('button:has-text("Start Pipeline")', {
      state: 'visible',
      timeout: EXIST_TIMEOUT,
    }).should.eventually.not.equal(null);
  });

  it('runs a computation', async () => {
    await appWindow.click('button:has-text("Start Pipeline")', { timeout: EXIST_TIMEOUT });

    // Assert
    return appWindow.waitForSelector(`span:has-text("Pipeline Starting for ${CONS_NAME}.")`, {
      state: 'visible',
      timeout: EXIST_TIMEOUT,
    }).should.eventually.not.equal(null);
  });

  it('displays computation progress', async () => {
    await appWindow.click('a:has-text("Home")', { timeout: EXIST_TIMEOUT });

    // Assert
    return appWindow.waitForSelector('div.run-item-paper:first-child span:has-text("In Progress")', {
      state: 'visible',
      timeout: EXIST_TIMEOUT,
    }).should.eventually.not.equal(null);
  });

  it('displays results', async () => {
    // Wait for computation to complete (results button only shows up at the end of the run)
    await appWindow.click('div.run-item-paper:first-child a:has-text("View Results")', { timeout: COMPUTATION_TIMEOUT });

    // Assert
    return Promise.all([
      appWindow.waitForSelector('h3:has-text("Regressions")', {
        state: 'visible',
        timeout: EXIST_TIMEOUT,
      }).should.eventually.not.equal(null),
      appWindow.waitForSelector(`h6:has-text("Results: ${CONS_NAME} || ${PIPE_NAME}")`, {
        state: 'visible',
        timeout: EXIST_TIMEOUT,
      }).should.eventually.not.equal(null),
    ]);
  });

  it('deletes consortium', async () => {
    await appWindow.click('a:has-text("Consortia")', { timeout: EXIST_TIMEOUT });

    await appWindow.click(`button[name="${CONS_NAME}-delete"]`, { timeout: EXIST_TIMEOUT });

    await appWindow.click('#list-delete-modal button:has-text("Delete")', { timeout: EXIST_TIMEOUT });

    // Assert

    // Wait for consortium item to be deleted from consortium list
    return appWindow.waitForSelector(`h1:has-text("${CONS_NAME}")`, {
      state: 'hidden',
      timeout: EXIST_TIMEOUT,
    }).should.eventually.equal(null);
  });

  it('logs out', async () => {
    await appWindow.click('a:has-text("Log Out")', { timeout: EXIST_TIMEOUT });

    // Assert
    return appWindow.waitForSelector('button:has-text("Log In")', {
      state: 'visible',
      timeout: EXIST_TIMEOUT,
    }).should.eventually.not.equal(null);
  });
});
<|endoftext|>