<commit_before>import {
  ContentNodeResource,
  ContentNodeProgressResource,
  UserExamResource,
  ExamLogResource,
  ExamAttemptLogResource,
} from 'kolibri.resources';

import { getChannelObject, isUserLoggedIn, currentUserId } from 'kolibri.coreVue.vuex.getters';
import {
  setChannelInfo,
  handleError,
  handleApiError,
  samePageCheckGenerator,
} from 'kolibri.coreVue.vuex.actions';
import { createQuestionList, selectQuestionFromExercise } from 'kolibri.utils.exams';
import { ContentNodeKinds } from 'kolibri.coreVue.vuex.constants';
import { PageNames, ClassesPageNames } from '../../constants';
import { assessmentMetaDataState } from 'kolibri.coreVue.vuex.mappers';
import { now } from 'kolibri.utils.serverClock';
import ConditionalPromise from 'kolibri.lib.conditionalPromise';
import router from 'kolibri.coreVue.router';
import seededShuffle from 'kolibri.lib.seededshuffle';
import { createTranslator } from 'kolibri.utils.i18n';
import { getContentNodeThumbnail } from 'kolibri.utils.contentNode';
import tail from 'lodash';

const translator = createTranslator('topicTreeExplorationPageTitles', {
  topicsForChannelPageTitle: 'Topics - { currentChannelTitle }',
  currentTopicForChannelPageTitle: '{ currentTopicTitle } - { currentChannelTitle }',
  currentContentForChannelPageTitle: '{ currentContentTitle } - { currentChannelTitle }',
  contentUnavailablePageTitle: 'Content Unavailable',
  searchPageTitle: 'Search',
  examsListPageTitle: 'Exams',
  currentExamPageTitle: '{ currentExamTitle} - { currentChannelTitle }',
});

// adds progress, thumbnail, and breadcrumbs. normalizes pk/id and kind
function normalizeContentNode(node, ancestors = []) {
  const normalized = {
    ...node,
    // TODO change serializer to use ID
    id: node.pk,
    kind: node.parent ? node.kind : ContentNodeKinds.CHANNEL,
    thumbnail: getContentNodeThumbnail(node) || undefined,
    breadcrumbs: tail(ancestors),
    progress: Math.min(node.progress_fraction || 0, 1.0),
  };
  delete normalized.pk;
  return normalized;
}

export function contentState(data, next_content = [], ancestors = []) {
  return {
    next_content,
    ...normalizeContentNode(data, ancestors),
    ...assessmentMetaDataState(data),
  };
}

function _collectionState(data) {
  return data.map(
    item =>
      item.kind === ContentNodeKinds.TOPICS ? normalizeContentNode(item) : contentState(item)
  );
}

/**
 * Cache utility functions
 *
 * These methods are used to manipulate client side cache to reduce requests
 */

export function updateContentNodeProgress(channelId, contentId, progressFraction) {
  /*
   * Update the progress_fraction directly on the model object, so as to prevent having
   * to cache bust the model (and hence the entire collection), because some progress was
   * made on this ContentNode.
   */
  ContentNodeResource.getModel(contentId).set({ progress_fraction: progressFraction });
}

export function setAndCheckChannels(store) {
  return setChannelInfo(store).then(
    channels => {
      if (!channels.length) {
        router.replace({ name: PageNames.CONTENT_UNAVAILABLE });
      }
      return channels;
    },
    error => {
      handleApiError(store, error);
      return error;
    }
  );
}

/**
 * Actions
 *
 * These methods are used to update client-side state
 */

export function showRoot(store) {
  const { memberships } = store.state.learnAppState;
  router.replace({ name: memberships.length > 0 ? ClassesPageNames : PageNames.TOPICS_ROOT });
}
<commit_msg>fix lodash/tail import and link<commit_after>import {
  ContentNodeResource,
  ContentNodeProgressResource,
  UserExamResource,
  ExamLogResource,
  ExamAttemptLogResource,
} from 'kolibri.resources';

import { getChannelObject, isUserLoggedIn, currentUserId } from 'kolibri.coreVue.vuex.getters';
import {
  setChannelInfo,
  handleError,
  handleApiError,
  samePageCheckGenerator,
} from 'kolibri.coreVue.vuex.actions';
import { createQuestionList, selectQuestionFromExercise } from 'kolibri.utils.exams';
import { ContentNodeKinds } from 'kolibri.coreVue.vuex.constants';
import { PageNames, ClassesPageNames } from '../../constants';
import { assessmentMetaDataState } from 'kolibri.coreVue.vuex.mappers';
import { now } from 'kolibri.utils.serverClock';
import ConditionalPromise from 'kolibri.lib.conditionalPromise';
import router from 'kolibri.coreVue.router';
import seededShuffle from 'kolibri.lib.seededshuffle';
import { createTranslator } from 'kolibri.utils.i18n';
import { getContentNodeThumbnail } from 'kolibri.utils.contentNode';
import tail from 'lodash/tail';

const translator = createTranslator('topicTreeExplorationPageTitles', {
  topicsForChannelPageTitle: 'Topics - { currentChannelTitle }',
  currentTopicForChannelPageTitle: '{ currentTopicTitle } - { currentChannelTitle }',
  currentContentForChannelPageTitle: '{ currentContentTitle } - { currentChannelTitle }',
  contentUnavailablePageTitle: 'Content Unavailable',
  searchPageTitle: 'Search',
  examsListPageTitle: 'Exams',
  currentExamPageTitle: '{ currentExamTitle} - { currentChannelTitle }',
});

// adds progress, thumbnail, and breadcrumbs. normalizes pk/id and kind
function normalizeContentNode(node, ancestors = []) {
  const normalized = {
    ...node,
    // TODO change serializer to use ID
    id: node.pk,
    kind: node.parent ? node.kind : ContentNodeKinds.CHANNEL,
    thumbnail: getContentNodeThumbnail(node) || undefined,
    breadcrumbs: tail(ancestors).map(bc => ({ id: bc.pk, ...bc })),
    progress: Math.min(node.progress_fraction || 0, 1.0),
  };
  delete normalized.pk;
  return normalized;
}

export function contentState(data, next_content = [], ancestors = []) {
  return {
    next_content,
    ...normalizeContentNode(data, ancestors),
    ...assessmentMetaDataState(data),
  };
}

function _collectionState(data) {
  return data.map(
    item =>
      item.kind === ContentNodeKinds.TOPICS ? normalizeContentNode(item) : contentState(item)
  );
}

/**
 * Cache utility functions
 *
 * These methods are used to manipulate client side cache to reduce requests
 */

export function updateContentNodeProgress(channelId, contentId, progressFraction) {
  /*
   * Update the progress_fraction directly on the model object, so as to prevent having
   * to cache bust the model (and hence the entire collection), because some progress was
   * made on this ContentNode.
   */
  ContentNodeResource.getModel(contentId).set({ progress_fraction: progressFraction });
}

export function setAndCheckChannels(store) {
  return setChannelInfo(store).then(
    channels => {
      if (!channels.length) {
        router.replace({ name: PageNames.CONTENT_UNAVAILABLE });
      }
      return channels;
    },
    error => {
      handleApiError(store, error);
      return error;
    }
  );
}

/**
 * Actions
 *
 * These methods are used to update client-side state
 */

export function showRoot(store) {
  const { memberships } = store.state.learnAppState;
  router.replace({
    name: memberships.length > 0 ? ClassesPageNames.ALL_CLASSES : PageNames.TOPICS_ROOT,
  });
}

{'fraction_non_alphanumeric': 0.10983884648006785, 'fraction_numerical': 0.0016963528413910093, 'mean_word_length': 4.557737627651218, 'pattern_counts': {'":': 0, '<': 3, '<?xml version=': 0, '>': 12, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '5522761', 'n_tokens_mistral': 2117, 'n_tokens_neox': 2120, 'n_words': 489}