예제 #1
0
def update_story(committer_id, story_id, change_list, commit_message):
    """Updates a story. Commits changes.

    # NOTE: This function should not be called on its own. Access it
    # through `topic_services.update_story_and_topic_summary`.

    Args:
        committer_id: str. The id of the user who is performing the update
            action.
        story_id: str. The story id.
        change_list: list(StoryChange). These changes are applied in sequence to
            produce the resulting story.
        commit_message: str or None. A description of changes made to the
            story.

    Raises:
        ValidationError. Exploration is already linked to a different story.
    """
    if not commit_message:
        raise ValueError('Expected a commit message but received none.')

    old_story = story_fetchers.get_story_by_id(story_id)
    new_story, exp_ids_removed_from_story, exp_ids_added_to_story = (
        apply_change_list(story_id, change_list))
    story_is_published = _is_story_published_and_present_in_topic(new_story)
    exploration_context_models_to_be_deleted = (
        exp_models.ExplorationContextModel.get_multi(
            exp_ids_removed_from_story))
    exploration_context_models_to_be_deleted = [
        model for model in exploration_context_models_to_be_deleted
        if model is not None
    ]
    exploration_context_models_collisions_list = (
        exp_models.ExplorationContextModel.get_multi(exp_ids_added_to_story))
    for context_model in exploration_context_models_collisions_list:
        if context_model is not None and context_model.story_id != story_id:
            raise utils.ValidationError(
                'The exploration with ID %s is already linked to story '
                'with ID %s' % (context_model.id, context_model.story_id))

    if (old_story.url_fragment != new_story.url_fragment
            and does_story_exist_with_url_fragment(new_story.url_fragment)):
        raise utils.ValidationError(
            'Story Url Fragment is not unique across the site.')
    _save_story(committer_id, new_story, commit_message, change_list,
                story_is_published)
    create_story_summary(new_story.id)
    if story_is_published and _is_topic_published(new_story):
        opportunity_services.update_exploration_opportunities(
            old_story, new_story)
    suggestion_services.auto_reject_translation_suggestions_for_exp_ids(
        exp_ids_removed_from_story)

    exp_models.ExplorationContextModel.delete_multi(
        exploration_context_models_to_be_deleted)

    new_exploration_context_models = [
        exp_models.ExplorationContextModel(id=exp_id, story_id=story_id)
        for exp_id in exp_ids_added_to_story
    ]
    exp_models.ExplorationContextModel.update_timestamps_multi(
        new_exploration_context_models)
    exp_models.ExplorationContextModel.put_multi(
        new_exploration_context_models)
예제 #2
0
def publish_story(topic_id, story_id, committer_id):
    """Marks the given story as published.

    Args:
        topic_id: str. The id of the topic.
        story_id: str. The id of the given story.
        committer_id: str. ID of the committer.

    Raises:
        Exception. The given story does not exist.
        Exception. The story is already published.
        Exception. The user does not have enough rights to publish the story.
    """
    def _are_nodes_valid_for_publishing(story_nodes):
        """Validates the story nodes before publishing.

        Args:
            story_nodes: list(dict(str, *)). The list of story nodes dicts.

        Raises:
            Exception: The story node doesn't contain any exploration id or the
                exploration id is invalid or isn't published yet.
        """
        exploration_id_list = []
        for node in story_nodes:
            if not node.exploration_id:
                raise Exception('Story node with id %s does not contain an '
                                'exploration id.' % node.id)
            exploration_id_list.append(node.exploration_id)
        explorations = exp_fetchers.get_multiple_explorations_by_id(
            exploration_id_list, strict=False)
        for node in story_nodes:
            if not node.exploration_id in explorations:
                raise Exception('Exploration id %s doesn\'t exist.' %
                                node.exploration_id)
        multiple_exploration_rights = (
            rights_manager.get_multiple_exploration_rights_by_ids(
                exploration_id_list))
        for exploration_rights in multiple_exploration_rights:
            if exploration_rights.is_private():
                raise Exception('Exploration with id %s isn\'t published.' %
                                exploration_rights.id)

    topic = topic_fetchers.get_topic_by_id(topic_id, strict=None)
    if topic is None:
        raise Exception('A topic with the given ID doesn\'t exist')
    user = user_services.UserActionsInfo(committer_id)
    if role_services.ACTION_CHANGE_STORY_STATUS not in user.actions:
        raise Exception(
            'The user does not have enough rights to publish the story.')

    story = story_fetchers.get_story_by_id(story_id, strict=False)
    if story is None:
        raise Exception('A story with the given ID doesn\'t exist')
    for node in story.story_contents.nodes:
        if node.id == story.story_contents.initial_node_id:
            _are_nodes_valid_for_publishing([node])

    topic.publish_story(story_id)
    change_list = [
        topic_domain.TopicChange({
            'cmd': topic_domain.CMD_PUBLISH_STORY,
            'story_id': story_id
        })
    ]
    _save_topic(committer_id, topic, 'Published story with id %s' % story_id,
                change_list)
    generate_topic_summary(topic.id)
예제 #3
0
파일: story_viewer.py 프로젝트: mfr88/oppia
    def post(self, story_id, node_id):
        if not constants.ENABLE_NEW_STRUCTURE_VIEWER_UPDATES:
            raise self.PageNotFoundException

        try:
            story_fetchers.get_node_index_by_story_id_and_node_id(
                story_id, node_id)
        except Exception as e:
            raise self.PageNotFoundException(e)

        story = story_fetchers.get_story_by_id(story_id)
        completed_nodes = story_fetchers.get_completed_nodes_in_story(
            self.user_id, story_id)
        completed_node_ids = [
            completed_node.id for completed_node in completed_nodes
        ]

        ordered_nodes = [
            node for node in story.story_contents.get_ordered_nodes()
        ]

        next_exp_ids = []
        next_node_id = None
        if not node_id in completed_node_ids:
            story_services.record_completed_node_in_story_context(
                self.user_id, story_id, node_id)

            completed_nodes = story_fetchers.get_completed_nodes_in_story(
                self.user_id, story_id)
            completed_node_ids = [
                completed_node.id for completed_node in completed_nodes
            ]

            for node in ordered_nodes:
                if node.id not in completed_node_ids:
                    next_exp_ids = [node.exploration_id]
                    next_node_id = node.id
                    break

        ready_for_review_test = False
        exp_summaries = (
            summary_services.get_displayable_exp_summary_dicts_matching_ids(
                next_exp_ids))

        # If there are no questions for any of the acquired skills that the
        # learner has completed, do not show review tests.
        acquired_skills = skill_fetchers.get_multi_skills(
            story.get_acquired_skill_ids_for_node_ids(completed_node_ids))

        acquired_skill_ids = [skill.id for skill in acquired_skills]
        questions_available = len(
            question_services.get_questions_by_skill_ids(
                1, acquired_skill_ids, False)) > 0

        learner_completed_story = len(completed_nodes) == len(ordered_nodes)
        learner_at_review_point_in_story = (
            len(exp_summaries) != 0 and
            (len(completed_nodes) & constants.NUM_EXPLORATIONS_PER_REVIEW_TEST
             == 0))
        if questions_available and (learner_at_review_point_in_story
                                    or learner_completed_story):
            ready_for_review_test = True

        return self.render_json({
            'summaries': exp_summaries,
            'ready_for_review_test': ready_for_review_test,
            'next_node_id': next_node_id
        })
예제 #4
0
def apply_change_list(story_id, change_list):
    """Applies a changelist to a story and returns the result.

    Args:
        story_id: str. ID of the given story.
        change_list: list(StoryChange). A change list to be applied to the given
            story.

    Returns:
        Story, list(str), list(str). The resulting story domain object, the
        exploration IDs removed from story and the exploration IDs added to
        the story.
    """
    story = story_fetchers.get_story_by_id(story_id)
    exp_ids_in_old_story = story.story_contents.get_all_linked_exp_ids()
    try:
        for change in change_list:
            if not isinstance(change, story_domain.StoryChange):
                raise Exception('Expected change to be of type StoryChange')
            if change.cmd == story_domain.CMD_ADD_STORY_NODE:
                story.add_node(change.node_id, change.title)
            elif change.cmd == story_domain.CMD_DELETE_STORY_NODE:
                story.delete_node(change.node_id)
            elif (change.cmd ==
                  story_domain.CMD_UPDATE_STORY_NODE_OUTLINE_STATUS):
                if change.new_value:
                    story.mark_node_outline_as_finalized(change.node_id)
                else:
                    story.mark_node_outline_as_unfinalized(change.node_id)
            elif change.cmd == story_domain.CMD_UPDATE_STORY_NODE_PROPERTY:
                if (change.property_name ==
                        story_domain.STORY_NODE_PROPERTY_OUTLINE):
                    story.update_node_outline(change.node_id, change.new_value)
                elif (change.property_name ==
                      story_domain.STORY_NODE_PROPERTY_TITLE):
                    story.update_node_title(change.node_id, change.new_value)
                elif (change.property_name ==
                      story_domain.STORY_NODE_PROPERTY_DESCRIPTION):
                    story.update_node_description(change.node_id,
                                                  change.new_value)
                elif (change.property_name ==
                      story_domain.STORY_NODE_PROPERTY_THUMBNAIL_FILENAME):
                    story.update_node_thumbnail_filename(
                        change.node_id, change.new_value)
                elif (change.property_name ==
                      story_domain.STORY_NODE_PROPERTY_THUMBNAIL_BG_COLOR):
                    story.update_node_thumbnail_bg_color(
                        change.node_id, change.new_value)
                elif (change.property_name ==
                      story_domain.STORY_NODE_PROPERTY_ACQUIRED_SKILL_IDS):
                    story.update_node_acquired_skill_ids(
                        change.node_id, change.new_value)
                elif (change.property_name ==
                      story_domain.STORY_NODE_PROPERTY_PREREQUISITE_SKILL_IDS):
                    story.update_node_prerequisite_skill_ids(
                        change.node_id, change.new_value)
                elif (change.property_name ==
                      story_domain.STORY_NODE_PROPERTY_DESTINATION_NODE_IDS):
                    story.update_node_destination_node_ids(
                        change.node_id, change.new_value)
                elif (change.property_name ==
                      story_domain.STORY_NODE_PROPERTY_EXPLORATION_ID):
                    story.update_node_exploration_id(change.node_id,
                                                     change.new_value)
            elif change.cmd == story_domain.CMD_UPDATE_STORY_PROPERTY:
                if (change.property_name == story_domain.STORY_PROPERTY_TITLE):
                    story.update_title(change.new_value)
                elif (change.property_name ==
                      story_domain.STORY_PROPERTY_THUMBNAIL_FILENAME):
                    story.update_thumbnail_filename(change.new_value)
                elif (change.property_name ==
                      story_domain.STORY_PROPERTY_THUMBNAIL_BG_COLOR):
                    story.update_thumbnail_bg_color(change.new_value)
                elif (change.property_name ==
                      story_domain.STORY_PROPERTY_DESCRIPTION):
                    story.update_description(change.new_value)
                elif (change.property_name == story_domain.STORY_PROPERTY_NOTES
                      ):
                    story.update_notes(change.new_value)
                elif (change.property_name ==
                      story_domain.STORY_PROPERTY_LANGUAGE_CODE):
                    story.update_language_code(change.new_value)
                elif (change.property_name ==
                      story_domain.STORY_PROPERTY_URL_FRAGMENT):
                    story.update_url_fragment(change.new_value)
                elif (change.property_name ==
                      story_domain.STORY_PROPERTY_META_TAG_CONTENT):
                    story.update_meta_tag_content(change.new_value)
            elif change.cmd == story_domain.CMD_UPDATE_STORY_CONTENTS_PROPERTY:
                if (change.property_name == story_domain.INITIAL_NODE_ID):
                    story.update_initial_node(change.new_value)
                if change.property_name == story_domain.NODE:
                    story.rearrange_node_in_story(change.old_value,
                                                  change.new_value)
            elif (change.cmd ==
                  story_domain.CMD_MIGRATE_SCHEMA_TO_LATEST_VERSION):
                # Loading the story model from the datastore into a
                # Story domain object automatically converts it to use the
                # latest schema version. As a result, simply resaving the
                # story is sufficient to apply the schema migration.
                continue

        exp_ids_in_modified_story = (
            story.story_contents.get_all_linked_exp_ids())
        exp_ids_removed_from_story = list(
            set(exp_ids_in_old_story).difference(exp_ids_in_modified_story))
        exp_ids_added_to_story = list(
            set(exp_ids_in_modified_story).difference(exp_ids_in_old_story))
        return story, exp_ids_removed_from_story, exp_ids_added_to_story

    except Exception as e:
        logging.error('%s %s %s %s' %
                      (e.__class__.__name__, e, story_id, change_list))
        python_utils.reraise_exception()
예제 #5
0
    def test_migration_job_converts_old_story(self):
        """Tests that the schema conversion functions work
        correctly and an old story is converted to new
        version.
        """
        # Generate story with old(v1) story contents data.
        self.save_new_story_with_story_contents_schema_v1(
            self.STORY_ID, 'image.svg', '#F8BF74', self.albert_id, 'A title',
            'A description', 'A note', self.TOPIC_ID)
        topic_services.add_canonical_story(
            self.albert_id, self.TOPIC_ID, self.STORY_ID)
        story_model = (
            story_models.StoryModel.get(self.STORY_ID))
        self.assertEqual(story_model.story_contents_schema_version, 1)
        self.assertEqual(
            story_model.story_contents,
            {
                'initial_node_id': 'node_1',
                'next_node_id': 'node_2',
                'nodes': [{
                    'acquired_skill_ids': [],
                    'destination_node_ids': [],
                    'exploration_id': None,
                    'id': 'node_1',
                    'outline': (
                        '<p>Value</p><oppia-noninteractive-math raw_l'
                        'atex-with-value="&amp;quot;+,-,-,+&amp;quot'
                        ';"></oppia-noninteractive-math>'),
                    'outline_is_finalized': False,
                    'prerequisite_skill_ids': [],
                    'title': 'Chapter 1'
                }]
            })
        story = story_fetchers.get_story_by_id(self.STORY_ID)
        self.assertEqual(story.story_contents_schema_version, 4)
        self.assertEqual(
            story.story_contents.to_dict(),
            self.MIGRATED_STORY_CONTENTS_DICT)

        # Start migration job.
        job_id = (
            story_jobs_one_off.StoryMigrationOneOffJob.create_new())
        story_jobs_one_off.StoryMigrationOneOffJob.enqueue(job_id)
        self.process_and_flush_pending_mapreduce_tasks()

        # Verify the story migrates correctly.
        updated_story = (
            story_models.StoryModel.get(self.STORY_ID))
        self.assertEqual(
            updated_story.story_contents_schema_version,
            feconf.CURRENT_STORY_CONTENTS_SCHEMA_VERSION)
        updated_story = (
            story_fetchers.get_story_by_id(self.STORY_ID))
        self.assertEqual(
            updated_story.story_contents_schema_version,
            feconf.CURRENT_STORY_CONTENTS_SCHEMA_VERSION)
        self.assertEqual(
            updated_story.story_contents.to_dict(),
            self.MIGRATED_STORY_CONTENTS_DICT)

        output = story_jobs_one_off.StoryMigrationOneOffJob.get_output(job_id)
        expected = [[u'story_migrated',
                     [u'1 stories successfully migrated.']]]
        self.assertEqual(expected, [ast.literal_eval(x) for x in output])
예제 #6
0
 def test_get_story_by_id_with_valid_ids_returns_correct_dict(self) -> None:
     expected_story = self.story.to_dict()
     story = story_fetchers.get_story_by_id(self.story_id)
     self.assertEqual(story.to_dict(), expected_story)
예제 #7
0
    def test_update_story_node_properties(self):
        changelist = [
            story_domain.StoryChange({
                'cmd': story_domain.CMD_ADD_STORY_NODE,
                'node_id': self.NODE_ID_2,
                'title': 'Title 2'
            }),
            story_domain.StoryChange({
                'cmd':
                story_domain.CMD_UPDATE_STORY_NODE_PROPERTY,
                'property_name':
                (story_domain.STORY_NODE_PROPERTY_DESTINATION_NODE_IDS),
                'node_id':
                self.NODE_ID_2,
                'old_value': [],
                'new_value': [self.NODE_ID_1]
            }),
            story_domain.StoryChange({
                'cmd': story_domain.CMD_UPDATE_STORY_NODE_OUTLINE_STATUS,
                'node_id': self.NODE_ID_2,
                'old_value': False,
                'new_value': True
            }),
            story_domain.StoryChange({
                'cmd':
                story_domain.CMD_UPDATE_STORY_CONTENTS_PROPERTY,
                'property_name': (story_domain.INITIAL_NODE_ID),
                'old_value':
                self.NODE_ID_1,
                'new_value':
                self.NODE_ID_2
            })
        ]
        story_services.update_story(self.USER_ID, self.STORY_ID, changelist,
                                    'Added story node.')
        story = story_fetchers.get_story_by_id(self.STORY_ID)
        self.assertEqual(story.story_contents.nodes[1].destination_node_ids,
                         [self.NODE_ID_1])
        self.assertEqual(story.story_contents.nodes[1].outline_is_finalized,
                         True)
        self.assertEqual(story.story_contents.nodes[1].title, 'Title 2')
        self.assertEqual(story.story_contents.initial_node_id, self.NODE_ID_2)
        self.assertEqual(story.story_contents.next_node_id, 'node_3')
        self.assertEqual(story.version, 3)

        story_summary = story_fetchers.get_story_summary_by_id(self.STORY_ID)
        self.assertEqual(story_summary.node_count, 2)

        changelist = [
            story_domain.StoryChange({
                'cmd': story_domain.CMD_DELETE_STORY_NODE,
                'node_id': self.NODE_ID_1
            }),
            story_domain.StoryChange({
                'cmd': story_domain.CMD_UPDATE_STORY_NODE_OUTLINE_STATUS,
                'node_id': self.NODE_ID_2,
                'old_value': True,
                'new_value': False
            }),
            story_domain.StoryChange({
                'cmd':
                story_domain.CMD_UPDATE_STORY_NODE_PROPERTY,
                'property_name': (story_domain.STORY_NODE_PROPERTY_TITLE),
                'node_id':
                self.NODE_ID_2,
                'old_value':
                'Title 2',
                'new_value':
                'Modified title 2'
            }),
        ]
        story_services.update_story(self.USER_ID, self.STORY_ID, changelist,
                                    'Removed a story node.')
        story_summary = story_fetchers.get_story_summary_by_id(self.STORY_ID)
        story = story_fetchers.get_story_by_id(self.STORY_ID)
        self.assertEqual(story_summary.node_count, 1)
        self.assertEqual(story.story_contents.nodes[0].title,
                         'Modified title 2')
        self.assertEqual(story.story_contents.nodes[0].destination_node_ids,
                         [])
        self.assertEqual(story.story_contents.nodes[0].outline_is_finalized,
                         False)
예제 #8
0
def publish_story(topic_id, story_id, committer_id):
    """Marks the given story as published.

    Args:
        topic_id: str. The id of the topic.
        story_id: str. The id of the given story.
        committer_id: str. ID of the committer.

    Raises:
        Exception. The given story does not exist.
        Exception. The story is already published.
        Exception. The user does not have enough rights to publish the story.
    """
    def _are_nodes_valid_for_publishing(story_nodes):
        """Validates the story nodes before publishing.

        Args:
            story_nodes: list(dict(str, *)). The list of story nodes dicts.

        Raises:
            Exception. The story node doesn't contain any exploration id or the
                exploration id is invalid or isn't published yet.
        """
        exploration_id_list = []
        for node in story_nodes:
            if not node.exploration_id:
                raise Exception('Story node with id %s does not contain an '
                                'exploration id.' % node.id)
            exploration_id_list.append(node.exploration_id)
        story_services.validate_explorations_for_story(exploration_id_list,
                                                       True)

    topic = topic_fetchers.get_topic_by_id(topic_id, strict=None)
    if topic is None:
        raise Exception('A topic with the given ID doesn\'t exist')
    user = user_services.get_user_actions_info(committer_id)
    if role_services.ACTION_CHANGE_STORY_STATUS not in user.actions:
        raise Exception(
            'The user does not have enough rights to publish the story.')

    story = story_fetchers.get_story_by_id(story_id, strict=False)
    if story is None:
        raise Exception('A story with the given ID doesn\'t exist')
    for node in story.story_contents.nodes:
        if node.id == story.story_contents.initial_node_id:
            _are_nodes_valid_for_publishing([node])

    topic.publish_story(story_id)
    change_list = [
        topic_domain.TopicChange({
            'cmd': topic_domain.CMD_PUBLISH_STORY,
            'story_id': story_id
        })
    ]
    _save_topic(committer_id, topic, 'Published story with id %s' % story_id,
                change_list)
    generate_topic_summary(topic.id)
    # Create exploration opportunities corresponding to the story and linked
    # explorations.
    linked_exp_ids = story.story_contents.get_all_linked_exp_ids()
    opportunity_services.add_new_exploration_opportunities(
        story_id, linked_exp_ids)
예제 #9
0
 def test_get_story_by_id(self):
     expected_story = self.story.to_dict()
     story = story_fetchers.get_story_by_id(self.STORY_ID)
     self.assertEqual(story.to_dict(), expected_story)
예제 #10
0
def apply_change_list(story_id, change_list):
    """Applies a changelist to a story and returns the result.

    Args:
        story_id: str. ID of the given story.
        change_list: list(StoryChange). A change list to be applied to the given
            story.

    Returns:
        Story. The resulting story domain object.
    """
    story = story_fetchers.get_story_by_id(story_id)
    try:
        for change in change_list:
            if not isinstance(change, story_domain.StoryChange):
                raise Exception('Expected change to be of type StoryChange')
            if change.cmd == story_domain.CMD_ADD_STORY_NODE:
                story.add_node(change.node_id, change.title)
            elif change.cmd == story_domain.CMD_DELETE_STORY_NODE:
                story.delete_node(change.node_id)
            elif (change.cmd ==
                  story_domain.CMD_UPDATE_STORY_NODE_OUTLINE_STATUS):
                if change.new_value:
                    story.mark_node_outline_as_finalized(change.node_id)
                else:
                    story.mark_node_outline_as_unfinalized(change.node_id)
            elif change.cmd == story_domain.CMD_UPDATE_STORY_NODE_PROPERTY:
                if (change.property_name ==
                        story_domain.STORY_NODE_PROPERTY_OUTLINE):
                    story.update_node_outline(change.node_id, change.new_value)
                elif (change.property_name ==
                      story_domain.STORY_NODE_PROPERTY_TITLE):
                    story.update_node_title(change.node_id, change.new_value)
                elif (change.property_name ==
                      story_domain.STORY_NODE_PROPERTY_ACQUIRED_SKILL_IDS):
                    story.update_node_acquired_skill_ids(
                        change.node_id, change.new_value)
                elif (change.property_name ==
                      story_domain.STORY_NODE_PROPERTY_PREREQUISITE_SKILL_IDS):
                    story.update_node_prerequisite_skill_ids(
                        change.node_id, change.new_value)
                elif (change.property_name ==
                      story_domain.STORY_NODE_PROPERTY_DESTINATION_NODE_IDS):
                    story.update_node_destination_node_ids(
                        change.node_id, change.new_value)
                elif (change.property_name ==
                      story_domain.STORY_NODE_PROPERTY_EXPLORATION_ID):
                    story.update_node_exploration_id(
                        change.node_id, change.new_value)
            elif change.cmd == story_domain.CMD_UPDATE_STORY_PROPERTY:
                if (change.property_name ==
                        story_domain.STORY_PROPERTY_TITLE):
                    story.update_title(change.new_value)
                elif (change.property_name ==
                      story_domain.STORY_PROPERTY_DESCRIPTION):
                    story.update_description(change.new_value)
                elif (change.property_name ==
                      story_domain.STORY_PROPERTY_NOTES):
                    story.update_notes(change.new_value)
                elif (change.property_name ==
                      story_domain.STORY_PROPERTY_LANGUAGE_CODE):
                    story.update_language_code(change.new_value)
            elif change.cmd == story_domain.CMD_UPDATE_STORY_CONTENTS_PROPERTY:
                if (change.property_name ==
                        story_domain.INITIAL_NODE_ID):
                    story.update_initial_node(change.new_value)
            elif (
                    change.cmd ==
                    story_domain.CMD_MIGRATE_SCHEMA_TO_LATEST_VERSION):
                # Loading the story model from the datastore into a
                # Story domain object automatically converts it to use the
                # latest schema version. As a result, simply resaving the
                # story is sufficient to apply the schema migration.
                continue
        return story

    except Exception as e:
        logging.error(
            '%s %s %s %s' % (
                e.__class__.__name__, e, story_id, change_list)
        )
        raise
예제 #11
0
    def post(self, story_id, node_id):
        story = story_fetchers.get_story_by_id(story_id)
        if story is None:
            logging.error('Could not find a story corresponding to '
                          '%s id.' % story_id)
            self.render_json({})
            return
        topic = topic_fetchers.get_topic_by_id(story.corresponding_topic_id)
        completed_nodes = story_fetchers.get_completed_nodes_in_story(
            self.user_id, story_id)
        completed_node_ids = [
            completed_node.id for completed_node in completed_nodes
        ]
        ordered_nodes = story.story_contents.get_ordered_nodes()

        (next_exp_ids, next_node_id,
         completed_node_ids) = (self._record_node_completion(
             story_id, node_id, completed_node_ids, ordered_nodes))

        ready_for_review_test = False
        exp_summaries = (
            summary_services.get_displayable_exp_summary_dicts_matching_ids(
                next_exp_ids))

        # If there are no questions for any of the acquired skills that the
        # learner has completed, do not show review tests.
        acquired_skills = skill_fetchers.get_multi_skills(
            story.get_acquired_skill_ids_for_node_ids(completed_node_ids))

        acquired_skill_ids = [skill.id for skill in acquired_skills]
        questions_available = len(
            question_services.get_questions_by_skill_ids(
                1, acquired_skill_ids, False)) > 0

        learner_completed_story = len(completed_node_ids) == len(ordered_nodes)
        learner_at_review_point_in_story = (
            len(exp_summaries) != 0
            and (len(completed_node_ids)
                 & constants.NUM_EXPLORATIONS_PER_REVIEW_TEST == 0))
        if questions_available and (learner_at_review_point_in_story
                                    or learner_completed_story):
            ready_for_review_test = True

        # If there is no next_node_id, the story is marked as completed else
        # mark the story as incomplete.
        if next_node_id is None:
            learner_progress_services.mark_story_as_completed(
                self.user_id, story_id)
        else:
            learner_progress_services.record_story_started(
                self.user_id, story.id)

        completed_story_ids = (
            learner_progress_services.get_all_completed_story_ids(
                self.user_id))
        story_ids_in_topic = []
        for story in topic.canonical_story_references:
            story_ids_in_topic.append(story.story_id)

        is_topic_completed = set(story_ids_in_topic).intersection(
            set(completed_story_ids))

        # If at least one story in the topic is completed,
        # mark the topic as learnt else mark it as partially learnt.
        if not is_topic_completed:
            learner_progress_services.record_topic_started(
                self.user_id, topic.id)
        else:
            learner_progress_services.mark_topic_as_learnt(
                self.user_id, topic.id)

        return self.render_json({
            'summaries': exp_summaries,
            'ready_for_review_test': ready_for_review_test,
            'next_node_id': next_node_id
        })