예제 #1
0
    def test_retrieval_of_multiple_explorations(self):
        exps = {}
        chars = 'abcde'
        exp_ids = ['%s%s' % (self.EXP_1_ID, c) for c in chars]
        for _id in exp_ids:
            exp = self.save_new_valid_exploration(_id, self.owner_id)
            exps[_id] = exp

        result = exp_fetchers.get_multiple_explorations_by_id(exp_ids)
        for _id in exp_ids:
            self.assertEqual(result.get(_id).title, exps.get(_id).title)

        # Test retrieval of non-existent ids.
        result = exp_fetchers.get_multiple_explorations_by_id(exp_ids +
                                                              ['doesnt_exist'],
                                                              strict=False)
        for _id in exp_ids:
            self.assertEqual(result.get(_id).title, exps.get(_id).title)

        self.assertNotIn('doesnt_exist', result)

        with self.assertRaisesRegexp(
                Exception,
                'Couldn\'t find explorations with the following ids:\n'
                'doesnt_exist'):
            exp_fetchers.get_multiple_explorations_by_id(exp_ids +
                                                         ['doesnt_exist'])
예제 #2
0
파일: suggestion.py 프로젝트: oppia/oppia
def _construct_exploration_suggestions(suggestions):
    """Returns exploration suggestions with current exploration content. This
    method assumes that the supplied suggestions represent changes that are
    still valid, e.g. the suggestions refer to content that still exist in the
    linked exploration.

    Args:
        suggestions: list(BaseSuggestion). A list of suggestions.

    Returns:
        list(dict). List of suggestion dicts with an additional
        exploration_content_html field representing the target
        exploration's current content.
    """
    suggestion_dicts = []
    exp_ids = {suggestion.target_id for suggestion in suggestions}
    exp_id_to_exp = exp_fetchers.get_multiple_explorations_by_id(list(exp_ids))
    for suggestion in suggestions:
        exploration = exp_id_to_exp[suggestion.target_id]
        content_html = exploration.get_content_html(
            suggestion.change.state_name, suggestion.change.content_id)
        suggestion_dict = suggestion.to_dict()
        suggestion_dict['exploration_content_html'] = content_html
        suggestion_dicts.append(suggestion_dict)
    return suggestion_dicts
예제 #3
0
    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)
예제 #4
0
def add_new_exploration_opportunities(story_id, exp_ids):
    """Adds new exploration opportunity into the model.

    Args:
        story_id: str. ID of the story.
        exp_ids: list(str). A list of exploration ids for which new
            opportunities are to be created. All exp_ids must be part of the
            given story.
    """
    story = story_fetchers.get_story_by_id(story_id)
    topic = topic_fetchers.get_topic_by_id(story.corresponding_topic_id)
    explorations = exp_fetchers.get_multiple_explorations_by_id(exp_ids)

    exploration_opportunity_summary_list = []
    for exp_id, exploration in explorations.items():
        node = story.story_contents.get_node_with_corresponding_exp_id(exp_id)

        audio_language_codes = set([
            language['id'] for language in constants.SUPPORTED_AUDIO_LANGUAGES
        ])

        complete_translation_languages = set(
            exploration.get_languages_with_complete_translation())

        incomplete_translation_language_codes = (
            audio_language_codes - complete_translation_languages)
        need_voice_artist_in_language_codes = complete_translation_languages

        if exploration.language_code in incomplete_translation_language_codes:
            # Removing exploration language from incomplete translation
            # languages list as exploration does not need any translation in
            # its own language.
            incomplete_translation_language_codes.discard(
                exploration.language_code)
            # Adding exploration language to voiceover required languages
            # list as exploration can be voiceovered in it's own language.
            need_voice_artist_in_language_codes.add(exploration.language_code)

        content_count = exploration.get_content_count()
        translation_counts = exploration.get_translation_counts()

        # TODO(#7376): Once the voiceover application functionality is
        # implemented change this method such that it also populates the
        # assigned_voice_artist_in_language_codes with the required data.

        exploration_opportunity_summary = (
            opportunity_domain.ExplorationOpportunitySummary(
                exp_id, topic.id, topic.name, story.id, story.title,
                node.title, content_count,
                list(incomplete_translation_language_codes),
                translation_counts, list(need_voice_artist_in_language_codes),
                []))

        exploration_opportunity_summary_list.append(
            exploration_opportunity_summary)

    _save_multi_exploration_opportunity_summary(
        exploration_opportunity_summary_list)
예제 #5
0
 def test_retrieval_of_multiple_uncached_explorations(self) -> None:
     exp_ids = [self.EXP_1_ID, self.EXP_2_ID, self.EXP_3_ID]
     caching_services.delete_multi(
         caching_services.CACHE_NAMESPACE_EXPLORATION, None, exp_ids)
     uncached_explorations = exp_fetchers.get_multiple_explorations_by_id(
         exp_ids, False)
     self.assertEqual(len(uncached_explorations), 3)
     for key in uncached_explorations:
         self.assertIn(key, uncached_explorations)
예제 #6
0
def regenerate_opportunities_related_to_topic(
        topic_id, delete_existing_opportunities=False):
    """Regenerates opportunity models which belongs to a given topic.

    Args:
        topic_id: str. The ID of the topic.
        delete_existing_opportunities: bool. Whether to delete all the existing
            opportunities related to the given topic.

    Returns:
        int. The number of opportunity models created.

    Raises:
        Exception. Failure to regenerate opportunities for given topic.
    """
    if delete_existing_opportunities:
        exp_opportunity_models = (
            opportunity_models.ExplorationOpportunitySummaryModel.get_by_topic(
                topic_id))
        opportunity_models.ExplorationOpportunitySummaryModel.delete_multi(
            exp_opportunity_models)

    topic = topic_fetchers.get_topic_by_id(topic_id)
    story_ids = topic.get_canonical_story_ids()
    stories = story_fetchers.get_stories_by_ids(story_ids)
    exp_ids = []
    non_existing_story_ids = []

    for index, story in enumerate(stories):
        if story is None:
            non_existing_story_ids.append(story_ids[index])
        else:
            exp_ids += story.story_contents.get_all_linked_exp_ids()

    exp_ids_to_exp = exp_fetchers.get_multiple_explorations_by_id(exp_ids,
                                                                  strict=False)
    non_existing_exp_ids = set(exp_ids) - set(exp_ids_to_exp.keys())

    if len(non_existing_exp_ids) > 0 or len(non_existing_story_ids) > 0:
        raise Exception(
            'Failed to regenerate opportunities for topic id: %s, '
            'missing_exp_with_ids: %s, missing_story_with_ids: %s' %
            (topic_id, list(non_existing_exp_ids), non_existing_story_ids))

    exploration_opportunity_summary_list = []
    for story in stories:
        for exp_id in story.story_contents.get_all_linked_exp_ids():
            exploration_opportunity_summary_list.append(
                create_exp_opportunity_summary(topic, story,
                                               exp_ids_to_exp[exp_id]))

    _save_multi_exploration_opportunity_summary(
        exploration_opportunity_summary_list)
    return len(exploration_opportunity_summary_list)
예제 #7
0
    def map(item):
        if not item.deleted:
            topic = topic_fetchers.get_topic_from_model(item)
            story_references = topic.get_all_story_references()
            interaction_ids = set()

            for story_reference in story_references:
                story = story_fetchers.get_story_by_id(
                    story_reference.story_id)
                exp_ids = story.story_contents.get_all_linked_exp_ids()
                explorations = exp_fetchers.get_multiple_explorations_by_id(
                    exp_ids)
                for exploration in explorations.values():
                    for state in exploration.states.values():
                        interaction_ids.add(state.interaction.id)
            yield ('%s (%s)' % (topic.name, topic.id), list(interaction_ids))
예제 #8
0
def add_new_exploration_opportunities(story_id, exp_ids):
    """Adds new exploration opportunity into the model.

    Args:
        story_id: str. ID of the story.
        exp_ids: list(str). A list of exploration ids for which new
            opportunities are to be created. All exp_ids must be part of the
            given story.
    """
    story = story_fetchers.get_story_by_id(story_id)
    topic = topic_fetchers.get_topic_by_id(story.corresponding_topic_id)
    explorations = exp_fetchers.get_multiple_explorations_by_id(exp_ids)

    exploration_opportunity_summary_list = []
    for exploration in explorations.values():
        exploration_opportunity_summary_list.append(
            _create_exploration_opportunity_summary(topic, story, exploration))
    _save_multi_exploration_opportunity_summary(
        exploration_opportunity_summary_list)
예제 #9
0
def _create_exploration_opportunities(story, topic, exp_ids):
    """Creates new exploration opportunities corresponding to the supplied
    story, topic, and exploration IDs.

    Args:
        story: Story. The story domain object corresponding to the exploration
            opportunities.
        topic: Topic. The topic domain object corresponding to the exploration
            opportunities.
        exp_ids: list(str). A list of exploration ids for which new
            opportunities are to be created. All exp_ids must be part of the
            given story.
    """
    explorations = exp_fetchers.get_multiple_explorations_by_id(exp_ids)
    exploration_opportunity_summary_list = []
    for exploration in explorations.values():
        exploration_opportunity_summary_list.append(
            _create_exploration_opportunity_summary(topic, story, exploration))
    _save_multi_exploration_opportunity_summary(
        exploration_opportunity_summary_list)
예제 #10
0
    def map(user_model):
        """Implements the map function for this job."""
        user_id = user_model.id
        contributions = user_models.UserContributionsModel.get(user_id)

        created_explorations = exp_fetchers.get_multiple_explorations_by_id(
            contributions.created_exploration_ids)
        if created_explorations:
            user_model.last_created_an_exploration = max(
                [model.created_on for model in created_explorations.values()])

        user_commits = (exp_models.ExplorationCommitLogEntryModel.query(
            exp_models.ExplorationCommitLogEntryModel.user_id == user_id
        ).order(-exp_models.ExplorationCommitLogEntryModel.created_on).fetch(1)
                        )

        if user_commits:
            user_model.last_edited_an_exploration = user_commits[0].created_on

        user_model.put()
예제 #11
0
def _construct_exploration_suggestions(suggestions):
    """Returns exploration suggestions with current exploration content.

    Args:
        suggestions: list(BaseSuggestion). A list of suggestions.

    Returns:
        list(dict). List of suggestion dicts with an additional
        exploration_content_html field representing the target
        exploration's current content. If the given suggestion refers to an
        invalid content ID in the current exploration (this can happen if that
        content was deleted after the suggestion was made), the corresponding
        suggestion dict will be omitted from the return value.
    """
    exp_ids = {suggestion.target_id for suggestion in suggestions}
    exp_id_to_exp = exp_fetchers.get_multiple_explorations_by_id(list(exp_ids))

    suggestion_dicts = []
    for suggestion in suggestions:
        available_states = exp_id_to_exp[suggestion.target_id].states
        content_id_exists = False

        # Checks whether the state name within change object of the suggestion
        # is actually available in the target entity being suggested to and
        # then find the availability of the content ID in the translatable
        # content. See more - https://github.com/oppia/oppia/issues/14339
        if suggestion.change.state_name in available_states:
            content_id_exists = available_states[
                suggestion.change.state_name].has_content_id(
                    suggestion.change.content_id)

        if content_id_exists:
            content_html = exp_id_to_exp[
                suggestion.target_id].get_content_html(
                    suggestion.change.state_name, suggestion.change.content_id)
            suggestion_dict = suggestion.to_dict()
            suggestion_dict['exploration_content_html'] = content_html
            suggestion_dicts.append(suggestion_dict)
    return suggestion_dicts
예제 #12
0
def _construct_exploration_suggestions(suggestions):
    """Returns exploration suggestions with current exploration content.

    Args:
        suggestions: list(BaseSuggestion). A list of suggestions.

    Returns:
        list(dict). List of suggestion dicts with an additional
        exploration_content_html field representing the target
        exploration's current content.
    """
    exp_ids = {suggestion.target_id for suggestion in suggestions}
    exp_id_to_exp = exp_fetchers.get_multiple_explorations_by_id(list(exp_ids))

    suggestion_dicts = []
    for suggestion in suggestions:
        content_html = exp_id_to_exp[suggestion.target_id].get_content_html(
            suggestion.change.state_name, suggestion.change.content_id)
        suggestion_dict = suggestion.to_dict()
        suggestion_dict['exploration_content_html'] = content_html
        suggestion_dicts.append(suggestion_dict)
    return suggestion_dicts
예제 #13
0
def validate_explorations_for_story(exp_ids, strict):
    """Validates the explorations in the given story and checks whether they
    are compatible with the mobile app and ready for publishing.

    Args:
        exp_ids: list(str). The exp IDs to validate.
        strict: bool. Whether to raise an Exception when a validation error
            is encountered. If not, a list of the error messages are
            returned. strict should be True when this is called before
            saving the story and False when this function is called from the
            frontend.

    Returns:
        list(str). The various validation error messages (if strict is
        False).

    Raises:
        ValidationError. Expected story to only reference valid explorations.
        ValidationError. Exploration with ID is not public. Please publish
            explorations before adding them to a story.
        ValidationError. All explorations in a story should be of the same
            category.
    """
    validation_error_messages = []

    # Strict = False, since the existence of explorations is checked below.
    exps_dict = (
        exp_fetchers.get_multiple_explorations_by_id(exp_ids, strict=False))

    exp_rights = (
        rights_manager.get_multiple_exploration_rights_by_ids(exp_ids))

    exp_rights_dict = {}

    for rights in exp_rights:
        if rights is not None:
            exp_rights_dict[rights.id] = rights.status

    for exp_id in exp_ids:
        if exp_id not in exps_dict:
            error_string = (
                'Expected story to only reference valid explorations, but found'
                ' a reference to an invalid exploration with ID: %s'
                % exp_id)
            if strict:
                raise utils.ValidationError(error_string)
            validation_error_messages.append(error_string)
        else:
            if exp_rights_dict[exp_id] != constants.ACTIVITY_STATUS_PUBLIC:
                error_string = (
                    'Exploration with ID %s is not public. Please publish '
                    'explorations before adding them to a story.'
                    % exp_id)
                if strict:
                    raise utils.ValidationError(error_string)
                validation_error_messages.append(error_string)

    if exps_dict:
        for exp_id in exp_ids:
            if exp_id in exps_dict:
                sample_exp_id = exp_id
                break
        common_exp_category = exps_dict[sample_exp_id].category
        for exp_id in exps_dict:
            exp = exps_dict[exp_id]
            if exp.category != common_exp_category:
                error_string = (
                    'All explorations in a story should be of the '
                    'same category. The explorations with ID %s and %s have'
                    ' different categories.' % (sample_exp_id, exp_id))
                if strict:
                    raise utils.ValidationError(error_string)
                validation_error_messages.append(error_string)
            try:
                validation_error_messages.extend(
                    exp_services.validate_exploration_for_story(exp, strict))
            except Exception as e:
                logging.exception(
                    'Exploration validation failed for exploration with ID: '
                    '%s. Error: %s' % (exp_id, e))
                raise Exception(e)

    return validation_error_messages
예제 #14
0
def validate_explorations_for_story(exp_ids, raise_error):
    """Validates the explorations in the given story and checks whether they
    are compatible with the mobile app.

    Args:
        exp_ids: list(str). The exp IDs to validate.
        raise_error: bool. Whether to raise an Exception when a validation error
            is encountered. If not, a list of the error messages are
            returned. raise_error should be True when this is called before
            saving the story and False when this function is called from the
            frontend.

    Returns:
        list(str). The various validation error messages (if raise_error is
        False).

    Raises:
        ValidationError. Expected story to only reference valid explorations.
        ValidationError. Exploration with ID is not public. Please publish
            explorations before adding them to a story.
        ValidationError. All explorations in a story should be of the same
            category.
        ValidationError. Invalid language found for exploration.
        ValidationError. Expected no exploration to have parameter values in it.
        ValidationError. Invalid interaction in exploration.
        ValidationError. RTE content in state of exploration with ID is not
            supported on mobile.
    """
    validation_error_messages = []

    # Strict = False, since the existence of explorations is checked below.
    exps_dict = (
        exp_fetchers.get_multiple_explorations_by_id(exp_ids, strict=False))

    exp_rights = (
        rights_manager.get_multiple_exploration_rights_by_ids(exp_ids))

    exp_rights_dict = {}

    for rights in exp_rights:
        if rights is not None:
            exp_rights_dict[rights.id] = rights.status

    for exp_id in exp_ids:
        if exp_id not in exps_dict:
            error_string = (
                'Expected story to only reference valid explorations, but found'
                ' a reference to an invalid exploration with ID: %s'
                % exp_id)
            if raise_error:
                raise utils.ValidationError(error_string)
            validation_error_messages.append(error_string)
        else:
            if exp_rights_dict[exp_id] != constants.ACTIVITY_STATUS_PUBLIC:
                error_string = (
                    'Exploration with ID %s is not public. Please publish '
                    'explorations before adding them to a story.'
                    % exp_id)
                if raise_error:
                    raise utils.ValidationError(error_string)
                validation_error_messages.append(error_string)

    if exps_dict:
        for exp_id in exp_ids:
            if exp_id in exps_dict:
                sample_exp_id = exp_id
                break
        common_exp_category = exps_dict[sample_exp_id].category
        for exp_id in exps_dict:
            exp = exps_dict[exp_id]
            if exp.category != common_exp_category:
                error_string = (
                    'All explorations in a story should be of the '
                    'same category. The explorations with ID %s and %s have'
                    ' different categories.' % (sample_exp_id, exp_id))
                if raise_error:
                    raise utils.ValidationError(error_string)
                validation_error_messages.append(error_string)
            if (
                    exp.language_code not in
                    android_validation_constants.SUPPORTED_LANGUAGES):
                error_string = (
                    'Invalid language %s found for exploration '
                    'with ID %s.' % (exp.language_code, exp_id))
                if raise_error:
                    raise utils.ValidationError(error_string)
                validation_error_messages.append(error_string)

            if exp.param_specs or exp.param_changes:
                error_string = (
                    'Expected no exploration to have parameter '
                    'values in it. Invalid exploration: %s' % exp.id)
                if raise_error:
                    raise utils.ValidationError(error_string)
                validation_error_messages.append(error_string)

            for state_name in exp.states:
                state = exp.states[state_name]
                if not state.interaction.is_supported_on_android_app():
                    error_string = (
                        'Invalid interaction %s in exploration '
                        'with ID: %s.' % (state.interaction.id, exp.id))
                    if raise_error:
                        raise utils.ValidationError(error_string)
                    validation_error_messages.append(error_string)

                if not state.is_rte_content_supported_on_android():
                    error_string = (
                        'RTE content in state %s of exploration '
                        'with ID %s is not supported on mobile.'
                        % (state_name, exp.id))
                    if raise_error:
                        raise utils.ValidationError(error_string)
                    validation_error_messages.append(error_string)

                if state.interaction.id == 'EndExploration':
                    recommended_exploration_ids = (
                        state.interaction.customization_args[
                            'recommendedExplorationIds'].value)
                    if len(recommended_exploration_ids) != 0:
                        error_string = (
                            'Exploration with ID: %s contains exploration '
                            'recommendations in its EndExploration interaction.'
                            % (exp.id))
                        if raise_error:
                            raise utils.ValidationError(error_string)
                        validation_error_messages.append(error_string)

    return validation_error_messages
예제 #15
0
def _save_story(committer_id, story, commit_message, change_list):
    """Validates a story and commits it to persistent storage. If
    successful, increments the version number of the incoming story domain
    object by 1.

    Args:
        committer_id: str. ID of the given committer.
        story: Story. The story domain object to be saved.
        commit_message: str. The commit message.
        change_list: list(StoryChange). List of changes applied to a story.

    Raises:
        ValidationError: An invalid exploration was referenced in the
            story.
        Exception: The story model and the incoming story domain
            object have different version numbers.
    """
    if not change_list:
        raise Exception(
            'Unexpected error: received an invalid change list when trying to '
            'save story %s: %s' % (story.id, change_list))

    story.validate()
    # Validate that all explorations referenced by the story exist.
    exp_ids = [
        node.exploration_id for node in story.story_contents.nodes
        if node.exploration_id is not None]

    # The first exp ID in the story to compare categories later on.
    sample_exp_id = exp_ids[0] if exp_ids else None

    # Strict = False, since the existence of explorations is checked below.
    exps_dict = (
        exp_fetchers.get_multiple_explorations_by_id(exp_ids, strict=False))

    for node in story.story_contents.nodes:
        if (node.exploration_id is not None) and (
                node.exploration_id not in exps_dict):
            raise utils.ValidationError(
                'Expected story to only reference valid explorations, '
                'but found an exploration with ID: %s (was it deleted?)' %
                node.exploration_id)

    if exps_dict:
        common_exp_category = exps_dict[sample_exp_id].category
        for exp_id in exps_dict:
            exp = exps_dict[exp_id]
            if exp.category != common_exp_category:
                raise utils.ValidationError(
                    'All explorations in a story should be of the '
                    'same category. The explorations with ID %s and %s have'
                    ' different categories.' % (sample_exp_id, exp_id))
            if (
                    exp.language_code not in
                    android_validation_constants.SUPPORTED_LANGUAGES):
                raise utils.ValidationError(
                    'Invalid language %s found for exploration with ID %s.'
                    % (exp.language_code, exp_id))
            if exp.param_specs or exp.param_changes:
                raise utils.ValidationError(
                    'Expected no exploration to have parameter values in'
                    ' it. Invalid exploration: %s' % exp.id)
            for state_name in exp.states:
                state = exp.states[state_name]
                if not state.interaction.is_supported_on_android_app():
                    raise utils.ValidationError(
                        'Invalid interaction %s in exploration with ID: %s.' %
                        (state.interaction.id, exp.id))

                if not state.is_rte_content_supported_on_android():
                    raise utils.ValidationError(
                        'RTE content in state %s of exploration with ID %s is '
                        'not supported on mobile.' % (state_name, exp.id))


    # Story model cannot be None as story is passed as parameter here and that
    # is only possible if a story model with that story id exists. Also this is
    # a private function and so it cannot be called independently with any
    # story object.
    story_model = story_models.StoryModel.get(story.id)
    if story.version > story_model.version:
        raise Exception(
            'Unexpected error: trying to update version %s of story '
            'from version %s. Please reload the page and try again.'
            % (story_model.version, story.version))
    elif story.version < story_model.version:
        raise Exception(
            'Trying to update version %s of story from version %s, '
            'which is too old. Please reload the page and try again.'
            % (story_model.version, story.version))

    topic = topic_fetchers.get_topic_by_id(
        story.corresponding_topic_id, strict=False)
    if topic is None:
        raise utils.ValidationError(
            'Expected story to only belong to a valid topic, but found an '
            'topic with ID: %s' % story.corresponding_topic_id)

    canonical_story_ids = topic.get_canonical_story_ids()
    additional_story_ids = topic.get_additional_story_ids()
    if story.id not in canonical_story_ids + additional_story_ids:
        raise Exception(
            'Expected story to belong to the topic %s, but it is '
            'neither a part of the canonical stories or the additional stories '
            'of the topic.' % story.corresponding_topic_id)

    story_model.description = story.description
    story_model.title = story.title
    story_model.notes = story.notes
    story_model.language_code = story.language_code
    story_model.story_contents_schema_version = (
        story.story_contents_schema_version)
    story_model.story_contents = story.story_contents.to_dict()
    story_model.corresponding_topic_id = story.corresponding_topic_id
    story_model.version = story.version
    change_dicts = [change.to_dict() for change in change_list]
    story_model.commit(committer_id, commit_message, change_dicts)
    memcache_services.delete(story_fetchers.get_story_memcache_key(story.id))
    story.version += 1