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'])
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
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)
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)
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)
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)
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))
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)
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)
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()
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
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
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
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
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